UP | HOME

A Three Body Problem
A MRKISS Library Example Program

Author: Mitch Richling
Updated: 2025-09-10 16:56:01
Generated: 2025-09-10 16:56:02

Copyright © 2025 Mitch Richling. All rights reserved.

Table of Contents

1. Introduction

The code for this example is found in MRKISS/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.

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 Moon to Earth mass ratio is \(\frac{100}{8145}\). We wish to model the movement of a small satellite with negligible mass. The position, \((x_1,x_2)\), and velocity, \((v_1,v_2)\), of this satellite 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\).

Another interesting set of initial conditions leads to a five lobe orbit at \(t=19.14045706162071\):

\[\begin{align*} (x_1,x_2) &= \left(0.87978, 0\right) \\ (v_1,v_2) & = (0, -0.3797) \end{align*}\]

References:

  • Arenstorf (1963); Periodic Solutions of the Restricted Three Body Problem Representing Analytic Continuations of Keplerian Elliptic Motions; American J. of Math. 85 (1); p27-35;
  • Butcher (2008); Numerical Methods for Ordinary Differential Equations. 2ed; p29-30;
  • Butcher (2016); Numerical Methods for Ordinary Differential Equations. 3ed; p28-31;
  • Hairer, Norsett & Wanner (2009). Solving Ordinary Differential Equations. I: Nonstiff Problems. p129-130;

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.

call fixed_t_steps(status, istats1, sol1, eq, t_iv, y_iv, param, a, b, c, t_end_o=t_end)
print '(a)', status_to_message(status)
call print_istats(status, istats1)
call print_solution(status, sol1, filename_o="tree_body_fixed_t_steps.csv", end_o=istats1(1))

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 not performed in the ODE solver, but directly by a visualization software platform – we simply provide the points and derivatives.

call adaptive_steps(status, istats1, sol1, eq, t_iv, y_iv, param, a, b, c, p, &
                         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);
print '(a)', status_to_message(status)
call print_istats(status, istats1)
call print_solution(status, sol1, filename_o="three_body_adaptive_steps-std.csv", end_o=istats1(1))

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. The most common use case is Hermite interpolation:

three_body_interp_adapt_path.png

The data for the image above was produced by this code:

call seq(status, sol2(1,:), from_o=0.0_rk, to_o=t_end);                                     ! Create new t values
print '(a)', status_to_message(status)
call interpolate_solution(status, istats2, sol2, sol1, eq, param, num_src_pts_o=istats1(1)) ! Preform the interpolation
call print_solution(status, sol2, filename_o="three_body_steps_adapt_std_interpolated.csv")

Less common is linear interpolation:

three_body_lin_interp_adapt_path.png

The data for the image above was produced by this code:

call seq(status, sol2(1,:), from_o=0.0_rk, to_o=t_end);
call interpolate_solution(status, istats2, sol2, sol1, eq, param, num_src_pts_o=istats1(1), linear_interp_o=.true.)
print '(a)', status_to_message(status)
call print_solution(status, sol2, filename_o="three_body_steps_adapt_std_interpolated_lin.csv")

Note that Hermite interpolation provides an O(3) solution to the IVP, but that may still be inappropriate if high accuracy solutions are required. Here we can see the errors in Hermite interpolation used above:

three_body_interp_adapt_error.png

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 aesthetic 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:

call fixed_t_steps(status, istats1, sol1, eq, t_iv, y_iv, param, dpa, dpb, dpc, t_end_o=t_end)
print '(a)', status_to_message(status)
call print_istats(status, istats1)
call print_solution(status, sol1, filename_o="fixed_t_steps-dp.csv", end_o=istats1(1))

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 adaptive_steps() 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:

! Example subroutine to adjust t_delta in an attempt 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,          intent(out) :: status, end_run
  real(kind=rk),    intent(out) :: new_t_delta
  integer,          intent(out) :: sdf_flags
  integer,          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
  end_run   = 0
  sdf_flags = 0
  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

This isn't a perfect solution as we make the assumption that the length of the difference in \(\mathbf{y}\mathrm{-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 fixed_y_steps() solvers.

We "wire up" the above subroutine into adaptive_steps() via the stepp_o argument.

call adaptive_steps(status, istats1, sol1, eq, t_iv, y_iv, param, a, b, c, p, &
                         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);
print '(a)', status_to_message(status)
call print_istats(status, istats1)
call print_solution(status, sol1, filename_o="three_body_adaptive_steps-fix-delta-steps.csv", end_o=istats1(1))

6. Truly Fixed Steps in y-space

We can achieve truly fixed step sizes in \(\mathbf{y}\mathrm{-space}\) with the fixed_y_steps() solvers. In the image below we see the difference between fixed steps in \(t\mathrm{-space}\) vs \(\mathbf{y}\mathrm{-space}\) – remember we 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 fixed_y_steps() 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.

call fixed_y_steps(status, istats1, sol1, eq, t_iv, y_iv, param, a, b, 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)
print '(a)', status_to_message(status)
call print_istats(status, istats1)

call print_solution(status, sol1, filename_o="three_body_fixed_y_steps.csv", end_o=istats1(1))

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

call sloppy_fixed_y_steps(status, istats1, sol1, eq, t_iv, y_iv, param, a, b, c, 0.0034_rk, .01_rk, &
                                y_delta_len_idxs_o=[1,2], y_sol_len_max_o=path_length)
print '(a)', status_to_message(status)
call print_istats(status, istats1)
call print_solution(status, sol1, filename_o="sloppy_fixed_y_steps.csv", end_o=istats1(1))

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. 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.

three_body_maxt.png

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

! Example subroutine replicating the functionality of t_max_o in adaptive_steps().
subroutine sp_max_t(status, end_run, sdf_flags, new_t_delta, pnt_idx, solution, t_delta, y_delta)
  integer,          intent(out) :: status
  integer,          intent(out) :: end_run
  real(kind=rk),    intent(out) :: new_t_delta
  integer,          intent(out) :: sdf_flags
  integer,          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
  sdf_flags = 0
  new_t_delta = -1.0_rk
  if ( solution(1, pnt_idx-1) + t_delta > t_max) then
     end_run = 1
  else
     end_run = 0
  end if
end subroutine sp_max_t

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

call adaptive_steps(status, istats1, sol1, eq, t_iv, y_iv, param, a, b, c, p, &
                         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);
print '(a)', status_to_message(status)
call print_istats(status, istats1)
call print_solution(status, sol1, filename_o="three_body_adaptive_steps-pho-t-max.csv", end_o=istats1(1))

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 adaptive_steps() 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 adaptive_steps() 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 adaptive_steps() two things: 1) Solve for the final \(\Delta{t}\) with sdf_o, and 2) quit after this solution.

! 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,          intent(out) :: status, end_run
  real(kind=rk),    intent(out) :: new_t_delta
  integer,          intent(out) :: sdf_flags
  integer,          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
  sdf_flags   = 0
  end_run     = 0
  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
     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
           end_run   = 1
        end if
     end if
  end if
end subroutine sp_cross_moon

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!

! 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
  implicit none
  integer,          intent(out) :: status
  real(kind=rk),    intent(out) :: dist
  integer,          intent(in)  :: sdf_flags
  real(kind=rk),    intent(in)  :: t, y(:)
  status = 0
  dist = 1.0_rk - norm2(y(1:2))
end subroutine sdf_cross_moon

As usual we wire these two functions up to adaptive_steps() via the stepp_o and sdf_o arguments.

call adaptive_steps(status, istats1, sol1, eq, t_iv, y_iv, param, a, b, c, p, &
                         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);
print '(a)', status_to_message(status)
call print_istats(status, istats1)
call print_solution(status, sol1, filename_o="three_body_adaptive_steps-isct.csv", end_o=istats1(1))

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, istats_size
  use            :: mrkiss_solvers_wt,              only: fixed_t_steps, fixed_y_steps, adaptive_steps, &
                                                          sloppy_fixed_y_steps, interpolate_solution
  use            :: mrkiss_utils,                   only: print_solution, seq, print_istats, status_to_message
  use            :: mrkiss_eerk_verner_9_8,         only: a, b, c, p
  use            :: mrkiss_eerk_dormand_prince_5_4, only: dpa=>a, dpb=>b, 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)               :: sol1(1+2*deq_dim, num_points), sol2(1+2*deq_dim, num_points)
  integer                     :: status, istats1(istats_size), istats2(istats_size)

  print '(a)', repeat('*', 120)
  print '(a)', "Fixed t_delta run V(9)"
  call fixed_t_steps(status, istats1, sol1, eq, t_iv, y_iv, param, a, b, c, t_end_o=t_end)
  print '(a)', status_to_message(status)
  call print_istats(status, istats1)
  call print_solution(status, sol1, filename_o="tree_body_fixed_t_steps.csv", end_o=istats1(1))

  print '(a)', repeat('*', 120)
  print '(a)', "Fixed t_delta run DP(5)"
  call fixed_t_steps(status, istats1, sol1, eq, t_iv, y_iv, param, dpa, dpb, dpc, t_end_o=t_end)
  print '(a)', status_to_message(status)
  call print_istats(status, istats1)
  call print_solution(status, sol1, filename_o="fixed_t_steps-dp.csv", end_o=istats1(1))

  print '(a)', repeat('*', 120)
  print '(a)', "Fixed y_delta run"
  call fixed_y_steps(status, istats1, sol1, eq, t_iv, y_iv, param, a, b, 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)
  print '(a)', status_to_message(status)
  call print_istats(status, istats1)

  call print_solution(status, sol1, filename_o="three_body_fixed_y_steps.csv", end_o=istats1(1))

  print '(a)', repeat('*', 120)
  print '(a)', "Sloppy Fixed y_delta run"
  call sloppy_fixed_y_steps(status, istats1, sol1, eq, t_iv, y_iv, param, a, b, c, 0.0034_rk, .01_rk, &
                                  y_delta_len_idxs_o=[1,2], y_sol_len_max_o=path_length)
  print '(a)', status_to_message(status)
  call print_istats(status, istats1)
  call print_solution(status, sol1, filename_o="sloppy_fixed_y_steps.csv", end_o=istats1(1))

  print '(a)', repeat('*', 120)
  print '(a)', "Adaptive run"
  sol1 = 0
  call adaptive_steps(status, istats1, sol1, eq, t_iv, y_iv, param, a, b, c, p, &
                           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);
  print '(a)', status_to_message(status)
  call print_istats(status, istats1)
  call print_solution(status, sol1, filename_o="three_body_adaptive_steps-std.csv", end_o=istats1(1))

  print '(a)', repeat('*', 120)
  print '(a)', "Hermite interpolation run"
  sol2 = 0
  call seq(status, sol2(1,:), from_o=0.0_rk, to_o=t_end);                                     ! Create new t values
  print '(a)', status_to_message(status)
  call interpolate_solution(status, istats2, sol2, sol1, eq, param, num_src_pts_o=istats1(1)) ! Preform the interpolation
  call print_solution(status, sol2, filename_o="three_body_steps_adapt_std_interpolated.csv")

  print '(a)', repeat('*', 120)
  print '(a)', "Linear interpolation run"
  sol2 = 0
  call seq(status, sol2(1,:), from_o=0.0_rk, to_o=t_end);
  call interpolate_solution(status, istats2, sol2, sol1, eq, param, num_src_pts_o=istats1(1), linear_interp_o=.true.)
  print '(a)', status_to_message(status)
  call print_solution(status, sol2, filename_o="three_body_steps_adapt_std_interpolated_lin.csv")

  print '(a)', repeat('*', 120)
  print '(a)', "Adaptive run w max y_delta length"
  call adaptive_steps(status, istats1, sol1, eq, t_iv, y_iv, param, a, b, c, p, &
                           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);
  print '(a)', status_to_message(status)
  call print_istats(status, istats1)
  call print_solution(status, sol1, filename_o="three_body_adaptive_steps-fix-delta-steps.csv", end_o=istats1(1))

  print '(a)', repeat('*', 120)
  print '(a)', "Adaptive run w max t"
  call adaptive_steps(status, istats1, sol1, eq, t_iv, y_iv, param, a, b, c, p, &
                           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);
  print '(a)', status_to_message(status)
  call print_istats(status, istats1)
  call print_solution(status, sol1, filename_o="three_body_adaptive_steps-pho-t-max.csv", end_o=istats1(1))

  print '(a)', repeat('*', 120)
  print '(a)', "Adaptive run w moon orbit hit"
  call adaptive_steps(status, istats1, sol1, eq, t_iv, y_iv, param, a, b, c, p, &
                           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);
  print '(a)', status_to_message(status)
  call print_istats(status, istats1)
  call print_solution(status, sol1, filename_o="three_body_adaptive_steps-isct.csv", end_o=istats1(1))

contains

  subroutine eq(status, dydt, t, y, param)
    integer,          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

  ! Example subroutine replicating the functionality of t_max_o in adaptive_steps().
  subroutine sp_max_t(status, end_run, sdf_flags, new_t_delta, pnt_idx, solution, t_delta, y_delta)
    integer,          intent(out) :: status
    integer,          intent(out) :: end_run
    real(kind=rk),    intent(out) :: new_t_delta
    integer,          intent(out) :: sdf_flags
    integer,          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
    sdf_flags = 0
    new_t_delta = -1.0_rk
    if ( solution(1, pnt_idx-1) + t_delta > t_max) then
       end_run = 1
    else
       end_run = 0
    end if
  end subroutine sp_max_t

  ! Example subroutine to adjust t_delta in an attempt 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,          intent(out) :: status, end_run
    real(kind=rk),    intent(out) :: new_t_delta
    integer,          intent(out) :: sdf_flags
    integer,          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
    end_run   = 0
    sdf_flags = 0
    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

  ! 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,          intent(out) :: status, end_run
    real(kind=rk),    intent(out) :: new_t_delta
    integer,          intent(out) :: sdf_flags
    integer,          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
    sdf_flags   = 0
    end_run     = 0
    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
       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
             end_run   = 1
          end if
       end if
    end if
  end subroutine sp_cross_moon

  ! 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
    implicit none
    integer,          intent(out) :: status
    real(kind=rk),    intent(out) :: dist
    integer,          intent(in)  :: sdf_flags
    real(kind=rk),    intent(in)  :: t, y(:)
    status = 0
    dist = 1.0_rk - norm2(y(1:2))
  end subroutine sdf_cross_moon

end program three_body

9.2. R Code

The images were produced with R.

adDat <- fread('three_body_adaptive_steps-std.csv')
ftDat <- fread('tree_body_fixed_t_steps.csv')
fyDat <- fread('three_body_fixed_y_steps.csv')
loDat <- fread('fixed_t_steps-dp.csv')
slDat <- fread('sloppy_fixed_y_steps.csv')
a2Dat <- fread('three_body_adaptive_steps-fix-delta-steps.csv')
a3Dat <- fread('three_body_adaptive_steps-pho-t-max.csv')
a4Dat <- fread('three_body_adaptive_steps-isct.csv')
aiDat <- fread('three_body_steps_adapt_std_interpolated.csv')
alDat <- fread('three_body_steps_adapt_std_interpolated_lin.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_path(data=aiDat, aes(x=y1, y=y2, col='Interpolated')) + 
  geom_point(data=adDat, aes(x=y1, y=y2, col='Adaptive')) +
  scale_colour_manual(values=c("Interpolated"="darkblue", "Adaptive"="red")) +
  labs(title='Restricted Three Body Problem', subtitle='Interpolated Adaptive Solution (Hermite)', 
       x=expression(x[1]), y=expression(x[2]), col='') +
  coord_fixed()
ggsave(filename='three_body_interp_adapt_path.png', plot=gp, width=1024, height=800, units='px', dpi=150)

gp <- ggplot() + 
  geom_path(data=alDat, aes(x=y1, y=y2, col='Interpolated')) + 
  geom_point(data=adDat, aes(x=y1, y=y2, col='Adaptive')) +
  scale_colour_manual(values=c("Interpolated"="darkblue", "Adaptive"="red")) +
  labs(title='Restricted Three Body Problem', subtitle='Interpolated Adaptive Solution (Linear)', 
       x=expression(x[1]), y=expression(x[2]), col='') +
  coord_fixed()
ggsave(filename='three_body_lin_interp_adapt_path.png', plot=gp, width=1024, height=800, units='px', dpi=150)

gp <- ggplot(rbind(data.table(t=ftDat$t, aerr=abs(aiDat$y1-ftDat$y1), bse=abs(ftDat$y1) , var='x1'),
                   data.table(t=ftDat$t, aerr=abs(aiDat$y2-ftDat$y2), bse=abs(ftDat$y2) , var='x2'),
                   data.table(t=ftDat$t, aerr=abs(aiDat$y3-ftDat$y3), bse=abs(ftDat$y3) , var='v1'),
                   data.table(t=ftDat$t, aerr=abs(aiDat$y4-ftDat$y4), bse=abs(ftDat$y4) , var='v2')) %>%
             filter(aerr>0 & bse>0) %>%
             mutate(rerr=aerr/bse)) + 
  geom_line(aes(x=t, y=rerr, col=var), linewidth=2, alpha=0.5) +
  scale_colour_manual(values=c("x1"="darkgreen", "x2"="darkblue", "v1"="darkgoldenrod", "v2"="darkred"),
                      labels=c(expression(x[1]), expression(x[2]), expression(v[1]), expression(v[2]))) +
  scale_y_log10() +
  labs(title='Interpolated Adaptive Solution', subtitle='Relative Error', x=expression(t), y='error', col='') 
ggsave(filename='three_body_interp_adapt_error.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=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='') +
  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='') +
  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='') +
  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)