0% found this document useful (0 votes)
30 views

Chapter 4.1-4.3

Uploaded by

Isis Mirandola
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
30 views

Chapter 4.1-4.3

Uploaded by

Isis Mirandola
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 46

Chapter 4

Solving Differential Equations

Abstract Differential equations play a central role in numerics. We introduce basic


algorithms for first and second-order initial value problems. This will allow us to
numerically solve many interesting problems, for instance, a body falling through
the atmosphere, two- and three-body problems, a simple model for galaxy collisions,
and the expansion of the Universe. Apart from learning how to deal with numerical
errors, mastering multi-dimensional arrays and complex operations are important
objectives of this chapter. Moreover, it is shown how to produce histograms and
three-dimensional plots.

4.1 Numerical Integration of Initial Value Problems

From Newton’s laws to Schrödinger’s equation: physics is packed with differen-


tial equations. While the majority are partial differential equations, such as Euler’s
equations of fluid dynamics, Maxwell’s equations for electromagnetic fields, and
Schrödinger’s equation for the wave function in quantum physics, there also are
many applications of ordinary differential equations. In contrast to partial differen-
tial equations, ordinary differential equations determine functions of a single variable.
In this section, you will learn how to solve such equations numerically.

4.1.1 First Order Differential Equations

A differential equation of first order determines a time-dependent function x(t) by


a relation between the function and its first derivative ẋ. An example is the equation
for radioactive decay:
ẋ = λx , (4.1)

© Springer Nature Switzerland AG 2021 105


W. Schmidt and M. Völschow, Numerical Python in Astronomy and Astrophysics,
Undergraduate Lecture Notes in Physics,
https://doi.org/10.1007/978-3-030-70347-9_4
106 4 Solving Differential Equations

The solution of this equation is x(t) = x0 e−λt , where x0 = x(0) is said to be the initial
value. While it is straightforward to solve a linear differential equation, in which all
terms are linear in x or ẋ, non-linear differential equations are more challenging.
Consider the Bernoulli equation1 :

ẋ = α(t)x + β(t)x ρ , (4.2)

where α(t) and β(t) are given functions and ρ is a real number. It encompasses
important differential equations as special cases, for example, Eq. (4.1) follows for
α(t) = λ, β(t) = 0, ρ = 0 and the logistic equation describing population dynamics
for α(t) = a, β(t) = b with constants a > 0, b < 0, and ρ = 2 (see Exercise 4.1).
From the viewpoint of numerics, Bernoulli-type equations are interesting because
an analytic solution is known and can be compared to numerical approximations.
In the following, we will attempt to numerically solve an example for a Bernoulli
equation from astrophysics, namely the equation for the radial expansion of a so-
called Strömgren sphere. When a hot, massive star is borne, it floods its surroundings
with strongly ionizing UV radiation. As a result, a spherical bubble of ionized hydro-
gen (H II) forms around the star. It turns out that ionization progresses as ionization
front, i.e. a thin spherical shell propagating outwards (see [4], Sect. 12.3). Inside the
shell, virtually all hydrogen is ionized. The radial propagation of the shell is described
by a differential equation for the time-dependent radius r (t) [9] (convince yourself
that this equation is a Bernoulli differential equation):
 
1 4π 3 2
ṙ = S∗ − r n0α (4.3)
4πr 2 n 0 3

Here, S∗ is the total number of ionizing photons (i.e. photons of energy greater
than 13.6 eV; see Sect. 3.2.1) per unit time, n 0 is the number density of neutral
hydrogen atoms (H I), and α ≈ 3.1 × 10−13 cm3 s−1 is the recombination coefficient.
Recombination of ionized hydrogen and electrons competes with ionization of neutral
hydrogen.2
Equation (4.3) can be rewritten in the form

rs3 − r 3
ṙ = n 0 α (4.4)
3r 2
where  1/3
3S∗
rs = (4.5)
4πn 20 α

1 Named after the Swiss mathematician Jakob Bernoulli, one of the pioneers of calculus in the late
17th century.
2 The recombination rate is proportional to the product of the number densities of hydrogen ions and

electrons, which is n 20 in the case of a fully ionized medium. The total number of recombinations
per unit time is obtained by multiplying with the volume of the sphere.
4.1 Numerical Integration of Initial Value Problems 107

is the Strömgren radius. The speed ṙ approaches zero for r → rs , i.e. the propagation
of the ionization front slows down and stalls at the Strömgren radius.3 How large is
a Strömgren sphere? Let us do the calculation for an O6 star (see also [4], example
12.4):
1 import numpy
2 import astropy.units as unit
3
4 n0 = 5000 * 1/unit.cm**3 # number density of HI
5 S = 1.6e49 * 1/unit.s # ionizing photons emitted per second
6 alpha = 3.1e-13 * unit.cm**3/unit.s # recombination coefficient
7
8 rs = (3*S/(4*np.pi * n0**2 * alpha))**(1/3)
9 print("Strmoegren radius = {:.2f}".format(rs.to(unit.pc)))
The photons emitted by the star per second can be estimated from the luminosity
and the peak of the Planck spectrum for an effective temperature Teff ≈ 4.5 × 104 K.
Astropy’s units module allows us to express the result in parsec without bothering
about conversion factors (see also Sect. 3.1.1):

Stroemgren radius = 0.26 pc

To integrate Eq. (4.4) in time, we can apply a linear approximation over a small
time interval [t, t + t] (also called time step):

r (t + t)  r (t) + ṙ (t)t (4.6)

In other words, it is assumed that the derivative ṙ changes only little over t and can
be approximated by the value at time t. For given r (t), we can then substitute the
right-hand side of the differential equation for ṙ (t):

rs3 − r (t)3
r (t + t)  r (t) + n 0 α t
3r (t)2

Starting with an initial value r (0) = r0 , this rule can be applied iteratively for tn =
nt. This is the basic idea of the Euler method.4 For a general first-order differential
equation
ẋ = f (t, x) , (4.7)

it can be written as iterative scheme

xn+1 = xn + f (tn , xn )t (4.8)

3 Infact, the sphere of ionized hydrogen begins to expand once ṙ drops below the speed of sound.
The expansion stops when it reaches pressure equilibrium with the surrounding neutral medium.
4 The method was introduced by Leonhard Euler in his influential textbook on calculus from 1768,

long before it was possible to routinely carry out numerical approximations with the help of com-
puters.
108 4 Solving Differential Equations

where xn = x(tn ) and n = 0, 1, 2, . . .


In the following code, we iteratively compute the function values rn in a for loop
and collect them in a NumPy array, which is useful for plotting:
10 n0_cgs = n0.value
11 alpha_cgs = alpha.value
12 rs_cgs = rs.value
13
14 # time step in s
15 dt = 100
16 n_steps = 1000
17
18 # intialization of arrays for t and r(t)
19 t = np.linspace(0, n_steps*dt, n_steps+1)
20 r = np.zeros(n_steps+1)
21
22 # start radius in cm
23 r[0] = 1e16
24
25 # Euler integration
26 for n in range(n_steps):
27 rdot = n0_cgs * alpha_cgs * \
28 (rs_cgs**3 - r[n]**3)/(3*r[n]**2)
29 r[n+1] = r[n] + rdot * dt
The loop begins with n=0 and the first array element r[0] defined in line 23. To
compute the next value using the Euler scheme, the time derivative rdot is evaluated
in lines 27–28. The new radius is then assigned to the next element of the array r.
The array length is given by the number of time steps n_steps plus one element for
the initial value (line 20). In lines 10–12, all parameters are converted to pure floating
point numbers because Astropy units cannot be used with individual array elements
(see Sect. 2.1.3). In contrast to Python lists, which support arbitrary elements, array
elements must be floating point numbers without additional attributes. As demon-
strated in Appendix B.1, there is a significant trade-off in terms of computational
efficiency and memory consumption. In the example above, it is understood that all
variables are in cgs units.
Since the right-hand side of Eq. (4.4) diverges for r = 0, the initial radius r0 must
be positive. A reasonable choice is a small fraction of rs , which is of the order of a
parsec (about 3 × 1018 cm). We set r0 = 1016 cm. Given this initial value, what is
an appropriate choice for the time step t? As a first guess, we set the time step to
100 s (see line 15). The numerical solution for 1000 time steps is plotted with the
following code as dashed line. The plot is shown in Fig. 4.1.
4.1 Numerical Integration of Initial Value Problems 109

Fig. 4.1 Numerical solution of the initial value problem (4.4) for a Strömgren sphere with r0 =
1016 cm using the Euler method (dashed line). The analytic solution for r0 = 0 is shown as solid
line

30 import matplotlib.pyplot as plt


31
32 fig = plt.figure(figsize=(6, 4), dpi=100)
33 plt.plot(t,
34 rs_cgs*(1.0 - np.exp(-n0_cgs*alpha_cgs*(t)))**(1/3),
35 linestyle=’-’ , color=’red’ , label="analytic")
36 plt.plot(t, r, linestyle=’--’ , color=’green’ , label="Euler")
37 plt.legend(loc=’lower right’)
38 plt.xlabel("$t$ [s]")
39 plt.ylabel("$r$ [cm]")
40 plt.savefig("stroemgren_cgs.pdf")

For comparison, the analytic solution [9]


 1/3
r (t) = rs 1 − e−n 0 αt (4.9)

is plotted in lines 33–35 (see solid line in Fig. 4.1). It agrees quite well with our
numerical solution, except for the discrepancy at early time. Actually, this is mainly
caused by different zero points of the time coordinates. For numerical integration, we
assume the initial value r (0) = r0 , while formula (4.9) implies r (0) = 0. This can
be fixed by shifting the time coordinate of the analytic solution such that r (0) = r0 .
Solving Eq. (4.9) with r = r0 for time yields

1  
t0 = log 1 − (r0 /rs )3 . (4.10)
n0α
110 4 Solving Differential Equations

By plotting r (t  ) = r (t − t0 ), where t  ≥ 0 corresponds to the time coordinate used


for numerical integration, you will find that the analytical solution is closely matched
by the numerical solution.
The coefficient 1/n 0 α appearing in Eq. (4.10) is a time scale, which can be inter-
preted as formation time ts of the Strömgren sphere. By plugging in the numbers,
41 ts = 1/(n0*alpha)
42 print("Time scale = {:.2f}".format(ts.to(unit.yr)))
we find

Time scale = 20.44 yr

For a radius of about 0.3 pc, this is very fast and indicates that the propagation speed
of the ionization front must be quite high (see Exercise 4.2). It turns out that the final
time of our numerical solution (1000 time steps) is only a tiny fraction of ts :
43 t[-1]*unit.s/ts
The output is

0.000155

As a result, we would need roughly 104 × 1000 ∼ 107 time steps to reach ts . This
is a very large number of steps. To compute the time evolution for a longer interval
of time, we can increase the time step t, which in turn reduces the number of time
steps. However, keep in mind that Eq. (4.8) is an approximation. We need to weigh
the reduction of computational cost (fewer steps) against the loss of accuracy (larger
time step).
For a sensible choice, it is important to be aware of the physical scales char-
acterizing the system. The differential equation for the time-dependent radius of a
Strömgren sphere can be expressed in terms of the dimensionless variables r̃ = r/rs
and t˜ = t/ts :
dr̃ 1 − r̃ 3
= . (4.11)
dt˜ 3r̃ 2
In this form, it is much easier to choose initial values and an appropriate integration
interval. The initial radius should be small compared to the Strömgren radius, for
example, r̃0 = 0.01. To follow the evolution of the sphere over the time scale ts , we
need to integrate at least over the interval [0, 1] with respect to t˜ (in the example above,
the interval was [0, 0.000155]). Obviously, the times step must be small compared
to ts , i.e. t/ts ≡ t˜
1. The following code computes and plots the numerical
solution for different time steps, starting with t˜ = 10−3 .
4.1 Numerical Integration of Initial Value Problems 111

44 # initial radius (dimensionless)


45 r0 = 0.01
46
47 # analytic solution
48 t0 = np.log(1 - r0**3)
49 t = np.arange(0, 2.0, 0.01)
50
51 fig = plt.figure(figsize=(6, 4), dpi=100)
52 plt.plot(t, (1.0 - np.exp(-t+t0))**(1/3),
53 color=’red’, label="analytic")
54
55 # time step (dimensionless)
56 dt = 1e-3
57 n_steps = 2000
58
59 while dt >= 1e-5:
60 t = np.linspace(0, n_steps*dt, n_steps+1)
61 r = np.zeros(n_steps+1)
62 r[0] = r0
63
64 print("Integrating {:d} steps for dt = {:.0e}".
65 format(n_steps,dt))
66 for n in range(n_steps):
67 rdot = (1 - r[n]**3)/(3*r[n]**2)
68 r[n+1] = r[n] + rdot * dt
69
70 # plot the data
71 plt.plot(t, r, linestyle=’--’ ,
72 label="Euler, $\Delta t$ = {:.1f}".
73 format(dt*ts.to(unit.hr)))
74
75 # decrease time step by a factor of 10
76 dt *= 0.1
77 n_steps *= 10
78
79 plt.legend(loc=’lower right’)
80 plt.xlabel("$t/t_{\mathrm{s}}$")
81 plt.ylabel("$r/r_{\mathrm{s}}$")
82 plt.ylim(0,1)
83 plt.savefig("stroemgren_dimensionless.pdf")
112 4 Solving Differential Equations

Fig. 4.2 Numerical solutions of the differential equation in dimensionless formulation (4.11) for
different time steps (t˜ = 10−3 , 10−4 , and 10−5 ; the corresponding physical time steps are indi-
cated in the legend). The Strömgren radius and formation time are rs = 0.26 pc and ts = 20.4 yr,
respectively

This examples shows how to plot multiple graphs from within a loop. The time step
is iterated in a while loop. The nested for loop starting at line 66 applies the Euler
scheme to Eq. (4.11). Here, the arrays t and r contain values of the dimensionless
variables t˜ and r̃ . It is important to create these arrays for each iteration of the outer
loop (see lines 60–61) because the number of time steps and, consequently, the array
size increases. After plotting r versus t, the time step is reduced by a factor of 10
(line 76) and the number of steps increases by a factor of 10 to reach the same end
point (line 77). The while loop terminates if t˜ < 10−5 . The physical time step in
hours is printed in the legend (the label is produced in lines 72–73). Prior to the loop,
we print the analytic solution with the shifted time coordinate defined by Eq. (4.10)
(see lines 48–53). The results are shown in Fig. 4.2.
The largest time step, t˜ = 10−3 clearly does not work. The initial speed goes
through the roof, and the radius becomes by far too large. The solution for t˜ = 10−4
improves somewhat, but the problem is still there. Only the smallest time step, t˜ =
10−5 , results in an acceptable approximation with small relative deviation from the
analytic solution. However, 200 000 steps are required in this case. It appears to be
quite challenging to solve this problem numerically. Can we do better than that?
The Euler method is the simplest method of solving a first order differential
equation. There are more sophisticated and more accurate methods. An example
is the Runge-Kutta method, which is a higher-order method.5 This means that the
discretization error for a finite time step t is of the order t n , where n > 2. In the

5 A different approach are variable step-size methods. The idea is to adapt the time step based on some

error estimate. An example is the Bulirsch-Stoer method. Such a method would be beneficial for
treating the initial expansion of a Strömgren sphere. However, with the speed of modern computers,
4.1 Numerical Integration of Initial Value Problems 113

case of the classic Runge-Kutta method, the error is of order t 5 . Thus, it is said to be
fourth-order accurate and in short called RK4.6 In contrast, the Euler method is only
first-order accurate. This can be seen by comparing the linear approximation (4.6)
to the Taylor series expansion

1
r (t + t) = r (t) + ṙ (t)t + r̈ (t)t 2 + . . .
2

The Euler method follows by truncating all terms of order t 2 and higher. For
this reason, the discretization error is also called truncation error. But how can we
extend a numerical scheme for an initial value problem to higher order? The first
order differential equation (4.7) determines the first derivative ẋ for given t and x,
but not higher derivatives (do not confuse the order with respect to the truncation
error and the order of the differential equation). Nevertheless, higher-order accuracy
can be achieved by combining different estimates of the slope ẋ at the subinterval
endpoints t and t + t and the midpoint t + t/2.
The RK4 scheme is defined by

k1 = f (t, x) t ,
k2 = f (t + t/2, x + k1 /2) t ,
k3 = f (t + t/2, x + k2 /2) t ,
k4 = f (t + t, x + k3 ) t ,

and
1
x(t + t) = x(t) + [k1 + 2(k2 + k3 ) + k4 ] . (4.12)
6
Here, k1 is the increment of x(t) corresponding to the Euler method, k2 /2 is the
interpolated increment for half of the time step, k3 /t is the corrected midpoint
slope based on x + k2 /2, and k4 /t the resulting slope at the subinterval endpoint
t + t. Equation (4.12) combines these estimates in a weighted average.
Let us put this into work. Since we need to compute multiple function values
for each time step, it is convenient to define the RK4 scheme as a Python function,
similar to the numerical integration schemes in Sect. 3.2.2:
1 def rk4_step(f, t, x, dt):
2
3 k1 = dt * f(t, x)
4 k2 = dt * f(t + 0.5*dt, x + 0.5*k1)
5 k3 = dt * f(t + 0.5*dt, x + 0.5*k2)

preference is given to higher-order methods. If you nevertheless want to learn more, you can find a
Fortran version of the Bulirsch-Stoer method in [10].
6 The (local) truncation error is an error per time step. The accumulated error or global truncation

error from the start to the endpoint of integration is usually one order lower, i.e. of order t 4 in
the case of the RK4 method.
114 4 Solving Differential Equations

6 k4 = dt * f(t + dt, x + k3)


7
8 return x + (k1 + 2*(k2 + k3) + k4)/6
Here, f() is a Python function corresponding to the right-hand side of Eq. (4.7).
In the following, you need to keep in mind that this function expects both the start
point t and the value x at the start point as arguments.
We can now compute numerical solutions of the initial value problem for the
Strömgren sphere using our implementation of RK4 with different time steps:
9 fig = plt.figure(figsize=(6, 4), dpi=100)
10 plt.plot(t, (1 - np.exp(-t+t0))**(1/3),
11 color=’red’ , label="analytic")
12
13 dt = 1e-3
14 n_steps = 2000
15
16 while dt >= 1e-5:
17 t = np.linspace(0, n_steps*dt, n_steps+1)
18 r = np.zeros(n_steps+1)
19 r[0] = r0
20
21 print("Integrating {:d} steps for dt = {:.0e}".
22 format(n_steps,dt), end=",")
23 for n in range(n_steps):
24 r[n+1] = rk4_step(lambda t, r: (1 - r**3)/(3*r**2),
25 t[n], r[n], dt)
26
27 # plot the new data
28 plt.plot(t, r, linestyle=’--’ ,
29 label="Runge-Kutta, $\Delta t$ = {:.1f}".
30 format(dt*ts.to(unit.hr)))
31
32 print(" endpoint deviation = {:.2e}".
33 format(r[-1] - (1 - np.exp(-t[-1]+t0))**(1/3)))
34
35 # decrease time step by a factor of 10
36 dt *= 0.1
37 n_steps *= 10
38
39 plt.legend(loc=’lower right’)
40 plt.xlabel("$t/t_{\mathrm{s}}$")
41 plt.ylabel("$r/r_{\mathrm{s}}$")
42 plt.ylim(0,1)
43 plt.savefig("stroemgren_rk4.pdf")
4.1 Numerical Integration of Initial Value Problems 115

Fig. 4.3 Same plot as in Fig. 4.2 with the Runge-Kutta (RK4) method instead of the Euler method

The Runge-Kutta integrator is called iteratively in lines 24–25. The values passed
to rk4_step() are t[n] and x[n]. Moreover, we have to specify the derivate
on the right-hand side of the differential equation. For the Strömgren sphere, the
mathematical definition reads

1 − r̃ 3
f (t˜, r̃ ) = .
3r̃ 2
This can be easily translated into a Python function. Since we need this function only
as input of rk4_step(), we use an anonymous function rather than a named func-
tion (see Sect. 3.2.2). The only pitfall is that our lambda must accept two arguments
(listed after the keyword lambda), even though only one of them occurs in the
expression following the colon (in other words, f (t˜, r̃ ) is actually independent of t˜).
Of course, the same would apply if we had used a named function. Both arguments
are necessary because the function is called with these two arguments in the body of
rk4_step() (see lines 3–6). You can try to remove the argument t in line 24 and
take a look at the ensuing error messages.
Compared to the first-order Euler method, the quality of the numerical solutions
computed with RK4 improves noticably. As shown in Fig. 4.3, the analytic solution
is closely reproduced for dt˜ = 10−4 or smaller. The deviation between the numerical
and analytic values at the end of integration is printed for each time step in lines
32–33:

Integrating 2000 steps for dt = 1e-03, endpoint deviation = 2.08e-01


Integrating 20000 steps for dt = 1e-04, endpoint deviation = 2.23e-04
Integrating 200000 steps for dt = 1e-05, endpoint deviation = 2.26e-07
116 4 Solving Differential Equations

The deviation decreases substantially for smaller time steps. While methods of higher
order are more accurate than methods of lower order, they require a larger number
of function evaluations. This may be costly depending on the complexity of the
differential equation. With four function evaluations, RK4 is often considered a
reasonable compromise between computational cost and accuracy. Other variants
of the Runge-Kutta method may require a larger or smaller number of evaluations,
resulting in higher or lower order. In Appendix B.2, optimization techniques are
explained that will enable you to speed up the execution of the Runge-Kutta step.
For the example discussed in this section, we know the analytic solution. This
allows us to test numerical methods. However, if we apply these methods to another
differential equation, can we be sure that the quality of the solution will be comparable
for a given time step? The answer is clearly no. One can get a handle on error
estimation from mathematical theory, but often it is not obvious how to choose an
appropriate time step for a particular initial value problem. For such applications, it
is important to check the convergence of the solution for successively smaller time
steps. If changes are small if the time step decreases, such as in the example above,
chances are good that the numerical solution is valid (although there is no guarantee
in the strict mathematical sense).

4.1.2 Second Order Differential Equations

In mechanics, we typically deal with second order differential equations of the form

ẍ = f (t, x, ẋ) , (4.13)

where x(t) is the unknown position function, ẋ(t) the velocity, and ẍ(t) the acceler-
ation of an object of mass m (which is hidden as parameter in the function f ). The
differential equation allows us to determine x(t) for a given initial position x0 = x(t0 )
and velocity v0 = ẋ(t0 ) at any subsequent time t > t0 . For this reason, it is called
equation of motion.
A very simple example is free fall:

ẍ = g , (4.14)

where x is the coordinate of the falling mass in vertical direction. Close to the surface
of Earth, g ≈ 9.81 m s−2 and f (t, x, ẋ) = g is constant. Of course, you know the
solution of the initial value problem for this equation:

1
x(t) = x0 + v0 (t − t0 ) + g(t − t0 )2 . (4.15)
2
For larger distances from ground, however, the approximation f (t, x, ẋ) = g is not
applicable and we need to use the 1/r gravitational potential of Earth. In this case,
4.1 Numerical Integration of Initial Value Problems 117

the right-hand side of the equation of motion is given by a position-dependent func-


tion f (x) and finding an analytic solution is possible, but slightly more difficult. In
general, f can also depend on the velocity ẋ, for example, if air resistance is taken
into account. We will study this case in some detail in Sect. 4.2 and you will learn
how to apply numerical methods to solve such a problem. An example, where f
changes explicitly with time t is a rocket with a time-dependent mass m(t).
To develop the tools we are going to apply in this chapter, we shall begin with
another second order differential equation for which an analytic solution is known.
An almost ubiquitous system in physics is the harmonic oscillator:

m ẍ + kx = 0 , (4.16)

which is equivalent to

k
ẍ = f (x) where f (x) = − x. (4.17)
m
The coefficient k is called spring constant for the archetypal oscillation of a mass
attached to a spring. You can easily check by substitution that the position function

x(t) = x0 cos(ω0 t) , (4.18)



where ω0 = k/m is the angular frequency, solves the initial value problem x(0) =
x0 and ẋ(0) = 0. An example from astronomy is circular orbital motion, for which
the Cartesian coordinate functions are harmonic oscillations with period T = 2π/ω
given by Kepler’s third law.
Similar to a body falling under the influence of air resistance, oscillations can be
damped by friction. Damping can be modeled by a velocity-dependent term7 :

m ẍ + d ẋ + kx = 0 , (4.19)

where d is the damping coefficient. Hence,8

d k
f (x, ẋ) = − ẋ − x , (4.20)
m m
This is readily translated into a Python function:
1 def xddot(t, x, xdot, m, d, k):
2 """
3 acceleration function of damped harmonic oscillator
4

7 An experimental realization would be a ball attached to a spring residing in an oil bath.


8 As a further generalization, a time-dependent, periodic force can act on the oscillator (forced
oscillation). In that case, f depends explicitly on time.
118 4 Solving Differential Equations

5 args: t - time
6 x - position
7 xdot - velocity
8 m - mass
9 d - damping constant
10 k - spring constant
11
12 returns: positions (unit amplitude)
13 """
14 return -(d*xdot + k*x)/m
In the following, we use a unit system that comes under the name of arbitrary units.
This is to say that we are not interested in the specific dimensions of the system,
but only in relative numbers. If you have the impression that this is just what we did
when normalizing radius and time for the Strömgren sphere in the previous section,
then you are absolutely right. Working with arbitrary units is just the lazy way of
introducing dimensionless quantities. From the programmer’s point of view, this
means that all variables are just numbers.
The Euler method introduced in Sect. 4.1.1 can be extended to second order initial
value problems by approximating the velocity difference over a finite time step t
as
v = ẍt  f (t, x, ẋ)t

and evaluating f (t, x, ẋ) at time t to obtain

ẋ(t + t)  ẋ(t) + v

The forward Euler method is then given by the iteration rules

xn+1 = xn + ẋn t , (4.21)


ẋn+1 = ẋn + f (tn , xn , ẋn )t (4.22)

starting from initial data x0 and ẋ0 . This is a specific choice. As we will see, there
are others.
To implement this method, we define a Python function similar to rk4_step()
for first-order differential equations:
15 euler_forward(f, t, x, xdot, h, *args):
16 """
17 Euler forward step for function x(t)
18 given by second order differential equation
19
20 args: f - function determining second derivative
21 t - value of independent variable t
22 x - value of x(t)
23 xdot - value of first derivative dx/dt
4.1 Numerical Integration of Initial Value Problems 119

24 h - time step
25 args - parameters
26
27 returns: iterated values for t + h
28 """
29 return ( x + h*xdot, xdot + h*f(t, x, xdot, *args) )
Compared to rk4_step(), there are two differences. First, the updated position
xn+1 and velocity ẋn+1 defined by Eqs. (4.21) and (4.22), respectively, are returned
as tuple. Second, euler_forward() admits so-called variadic arguments. The
following example shows how to use it:
30 import numpy as np
31
32 # parameters
33 m = 1.
34 d = 0.05
35 k = 0.5
36 x0 = 10
37
38 n = 1000 # number of time steps
39 dt = 0.05 # time step
40 t = np.arange(0, n*dt, dt)
41
42 # intialization of data arrays for numerical solutions
43 x_fe = np.zeros(n)
44 v_fe = np.zeros(n)
45
46 # initial data for t = 0
47 x_fe[0], v_fe[0] = x0, 0
48
49 # numerical integration using the forward Euler method
50 # parameters are passed as variadic arguments
51 for i in range(n-1):
52 x_fe[i+1], v_fe[i+1] = \
53 euler_forward(xddot, t[i], x_fe[i], v_fe[i], dt,
54 m, d, k)
The difficulty in writing a numerical solver in a generic way is that functions such as
xddot() can have any number of parameters. In the case of the damped oscillator,
these parameters are m, d, and k. For some other system, there might be fewer or more
parameters or none at all. We faced the same problem when integrating the Planck
spectrum in Sect 3.2.2. In this case, we used a Python lambda to convert a function
with an additional parameter (the effective temperature) to a proxy function with a
single argument (the wavelength, which is the integration variable). This maintains
120 4 Solving Differential Equations

a clear interface since all arguments are explicitly specified both in the definition
and in the calls of a function. A commonly used alternative is argument packing.
In our implementation of the forward Euler scheme, *args is a placeholder for
multiple positional arguments that are not explicitly specified as formal arguments
of the function. In other words, we can pass a varying number of arguments whenever
the function is called.9 For this reason, they are also called variadic arguments. You
have already come across variadic arguments in Sect. 3.1.1. In the example above,
m, d, and k are variadic arguments of euler_forward(). Why do we need them
here? Because xddot() expects these arguments and so we need to to pass them
to its counterpart f() inside euler_forward() (see line 53–54 and line 29 in
the definition above).
The for loop beginning in line 51 iterates positions and velocities and stores the
values after each time step in the arrays x_fe and v_fe. The arrays are initialized
with zeros for a given number of time steps (lines 43–44). The time array t defined in
line 40 is not needed for numerical integration, but for plotting the solution. The result
is shown as dashed line in Fig. 4.4 (the code for producing the plot is listed below, after
discussing further integration methods). Compared to the analytic solution for the
parameters chosen in our example (solid line), the oscillating shape with gradually
decreasing amplitude is reproduced, but the decline is too slow.
Before we tackle this problem, let us first take a look at the computation of the
analytic solution. For x(0) = x0 and ẋ(0) = 0, it can be expressed as
 γ
x(t) = x0 e−γ t cos(ωt) + sin(ωt) , (4.23)
ω

where γ = d/2m and the angular frequency is given by ω = ω02 − γ 2 (i.e. it is


lower than the frequency of the undamped harmonic oscillator). The solution is a
damped oscillation with exponentially decreasing amplitude only if γ < ω0 . Other-
wise, the oscillation is said to be overdamped and x(t) is just an exponential function.
We check for this case and ensure that all parameters are positive by means of excep-
tion handling in a Python function evaluating x(t)/x0 :
55 def osc(t, m, d, k):
56 """
57 normalized damped harmonic oscillator
58 with zero velocity at t = 0
59
60 args: t - array of time values
61 m - mass
62 d - damping constant
63 k - spring constant

9 Technically speaking, the arguments are packed into a tuple whose name is args (you can choose

any other name). This tuple is unpacked into the individual arguments via the unpacking operator *.
To see the difference, you can insert print statements for both args and *args in the body of
euler_forward() and execute a few steps.
4.1 Numerical Integration of Initial Value Problems 121

Fig. 4.4 Numerical and analytic solutions of differential equation (4.19) for a damped harmonic
oscillator with m = 1, d = 0.05, k = 0.5, x0 = 10, and ẋ0 = 0 (top plot). The relative deviations
from the analytic solution are shown in the bottom plot

64
65 returns: positions (unit amplitude)
66 """
67 try:
68 if m > 0 and d > 0 and k > 0: # positive parameters
69 gamma = 0.5*d/m
70 omega0 = np.sqrt(k/m)
71 if omega0 >= gamma: # underdamped or critical
72 # frequency of damped oscillation
73 omega = np.sqrt(omega0**2 - gamma**2)
122 4 Solving Differential Equations

74 print("Angular frequency = {:.6e}".


75 format(omega))
76 return np.exp(-gamma*t) * \
77 (np.cos(omega*t) +
78 gamma*np.sin(omega*t)/omega)
79 else:
80 raise ValueError
81 else:
82 raise ValueError
83
84 except ValueError:
85 print("Invalid argument: non-positive parameters
86 or overdamped")
87 return None
Invalid arguments are excluded by raising a ValueError as exception (see
Sect. 3.2.2).
All we need to do for a comparison between Eq. (4.23) and our numerical solution
is to call osc() for the time sequence defined by the array t and multiply the
normalized displacements returned by the function with x0:
88 # analytic solution
89 x = x0*osc(t, m, d, k)
90
91 # relative deviation
92 dev_fe = np.fabs((x - x_fe)/x)
When the function osc() is called with valid parameters, it prints the frequency of
the damped oscillation:

Angular frequency = 7.066647e-01

The relative deviation from the solution computed with the forward Euler scheme
is stored in dev_fe and plotted in Fig. 4.4. The accuracy of the numerical solution
is clearly not convincing. The average error increases from a few percent to almost
100 % after a few oscillations.
How can we improve the numerical solution? For sure, a smaller time step will
reduce the error (it is left as an exercise, to vary the time step and check how this
influences the deviation from the analytic solution). But we can also try to come
up with a better scheme. It turns out that a small modification of the Euler scheme
is already sufficient to significantly improve the solution for a given time step. The
forward Euler scheme approximates xn+1 and ẋn+1 at time tn + t by using the
slopes ẋn and f (tn , xn , ẋn ) at time tn . This is an example for an explicit scheme:
4.1 Numerical Integration of Initial Value Problems 123

values at later time depend only on values at earlier time. What if we used the new
velocity ẋn+1 rather than ẋn to calculate the change in position? In this case, the
iteration scheme is semi-implicit:

ẋn+1 = ẋn + f (tn , xn , ẋn )t (4.24)


xn+1 = xn + ẋn+1 t (4.25)

This scheme is also called symplectic Euler method and still a method of first
order. Nevertheless, Fig. 4.4 shows a significant improvement over the forward Euler
method. Most importantly, the typical error does not increase with time. This is a
property of symplectic solvers.10 Moreover, the relative error is smallest near the
minima and maxima of x(t). Owing to divisions by small numbers, the deviation has
peaks close to the zeros of x(t). Basically, this causes only small shifts of the times
where the numerical solution crosses x = 0.
Numerical mathematicians have come up with much more complex methods
to achieve higher accuracy. For example, the Runge-Kutta scheme discussed in
Sect. 4.1.1 can be extended to second-order differential equations. Moreover, it can
be generalized to a class of explicit fourth-order methods, which are known as Runge-
Kutta-Nyström (RKN4) methods:


4
ẋn+1 = ẋn + t ċi f ni , (4.26)
i=0


4
xn+1 = xn + t ẋn + t 2 ci f ni , (4.27)
i=0

where ci and ċi are method-specific coefficients and f i are evaluations of the accel-
eration functions (4.13) at times, positions, and velocities given by

f n0 = f (tn , xn , ẋn ) , (4.28)


⎛ ⎞

i−1
i−1
f ni = f ⎝tn + αi t, xn + ẋn αi t + t 2 γi j f n j , ẋn + t βi j f n j ⎠ .
j=0 j=0
(4.29)

with coefficients αi , γi j and βi j for i = 1, . . . , 4 and j ≤ i. These coefficient vector


and matrices determine the times, positions, and velocities within a particular time
step for which accelerations are evaluated. For an explicit method, the matrices must
be lower triangular. You can think of the expressions for positions and velocities in

10 The term symplectic originates from Hamiltonian systems. In fact, the reformulation of the second-

order differential equation (4.19) as a set of equations for position and velocity is similar to Hamil-
ton’s equations. Symplectic integrators are phase space preserving. This is why the accumulated
error does not grow in time.
124 4 Solving Differential Equations

parentheses and in Eqs. (4.26) and (4.26) as constant-acceleration formulas, assuming


different accelerations over the time step t. The acceleration values f ni are similar
to ki /t in the case of the simple Runge-Kutta method introduced in Sect. 4.1.1.
A particular flavor of the RKN4 method (i.e. with specific choices for the coeffi-
cients) is implemented in a module accompanying this book. Extract numkit.py
from the zip archive for this chapter. In this file you can find the definition of the func-
tion rkn4_step() for the RKN4 method along with other methods, for example,
euler_step() for the symplectic Euler method. Numerical methods form Sect. 3
are also included. To use any of these functions, you just need to import them.
In the code listed below, you can see how rkn4_step() and euler_step()
are applied to the initial value problem for the damped harmonic oscillator. While
the code for the Euler method can be easily understood from Eqs. (4.24) and (4.25),
we do not go into the gory details of the implementation of RKN4 here. Since the
acceleration f ni defined by Eq. (4.29) for a given index i depends on the accelerations
f n j with lower indices j (see the sums over j), array operations cannot be used and
elements must be computed subsequently in nested for loops. This can be seen
in the body of rkn4_step() when opening the module file in an editor or using
Jupyter. The coefficients are empirical in the sense that they are chosen depending
on the performance of the numerical integrator in various tests.
93 # apply symplectic Euler and RKN4 schemes
94 from numkit import euler_step, rkn4_step
95
96 x_rkn4 = np.zeros(n)
97 v_rkn4 = np.zeros(n)
98
99 x_se = np.zeros(n)
100 v_se = np.zeros(n)
101
102 x_rkn4[0], v_rkn4[0] = x0, 0
103 x_se[0], v_rkn4[0] = x0, 0
104
105 for i in range(n-1):
106 x_rkn4[i+1], v_rkn4[i+1] = \
107 rkn4_step(xddot, t[i], x_rkn4[i], v_rkn4[i], dt,
108 m, d, k)
109 x_se[i+1], v_se[i+1] = \
110 euler_step(xddot, t[i], x_se[i], v_se[i], dt,
111 m, d, k)
112
113 dev_rkn4 = np.fabs((x - x_rkn4)/x)
114 dev_se = np.fabs((x - x_se)/x)
4.1 Numerical Integration of Initial Value Problems 125

Is a numerical scheme as complicated as RKN4 worth the effort? Let us take a look
a the solutions plotted with the following code (see Fig. 4.4):
115 import matplotlib.pyplot as plt
116 %matplotlib inline
117
118 T = 2*np.pi/7.066647e-01 # period
119
120 fig = plt.figure(figsize=(6, 4), dpi=100)
121 plt.plot(t/T, x, linestyle=’-’ , color=’red’ ,
122 label="analytic")
123 plt.plot(t/T, x_fe, linestyle=’--’ , color=’orange’ ,
124 label="Euler forward")
125 plt.plot(t/T, x_se, linestyle=’--’ , color=’green’ ,
126 label="Euler symplectic")
127 plt.plot(t/T, x_rkn4, linestyle=’--’ , color=’mediumblue’ ,
128 label="RKN4")
129 plt.legend(loc=’upper right’)
130 plt.xlabel("$t$")
131 plt.ylabel("$x$")
132 plt.savefig("oscillator.pdf")
133
134 fig = plt.figure(figsize=(6, 4), dpi=100)
135 plt.semilogy(t/T, dev_fe, linestyle=’-’, color=’orange’,
136 label=’Euler forward’)
137 plt.semilogy(t/T, dev_se, linestyle=’-’, color=’green’,
138 label=’Euler symplectic’)
139 plt.semilogy(t/T, dev_rkn4, linestyle=’-’ , color=’mediumblue’,
140 label=’RKN4’)
141 plt.legend(loc=’right’)
142 plt.xlabel("$t$")
143 plt.ylabel("deviation")
144 plt.savefig("oscillator_delta.pdf")

We have already discussed the two variants of the Euler method (forward and sym-
plectic). If we just look at the plot showing x(t) for the different solvers, it appears
that we do not gain much by using RKN4 instead of the symplectic Euler solver.
However, the relative deviations reveal that the accuracy of the solution improves
by more than six orders of magnitude with the forth-order Runge-Kutta-Nyström
method. Although this method is quite a bit more complicated than the Euler
method, it requires only five function evaluations vs one evaluation. As explained
in Appendix B.1, you can apply the magic command %timeit to investigate how
execution time is affected. Apart from that, one can also see that the errors are phase
shifted compared to the oscillation (i.e. minimal and maximal errors do not coincide
with extrema or turning points of x(t)) and the error gradually increase with time.
In fact, Runge-Kutta-Nyström solvers are not symplectic. As always in numerics,
you need to take the requirements of your application into consideration and weigh
computational costs against gains.
126 4 Solving Differential Equations

Exercises

4.1 An important Bernoulli-type equation is the logistic differential equation

ẋ = kx(1 − x) , (4.30)

with the analytic solution

1
x(t) =  . (4.31)
1 − e−kt 1
x(0)
−1

This function has far reaching applications, ranging from population growth over
chemical reactions to artificial neuronal networks. Compute numerical solutions of
equation (4.30) for k = 0.5, k = 1, and k = 2. Vary the time step in each case and
compare to the analytic solution. Suppose the relative error at the time t = 10/k
should not exceed 10−3 . How many time steps do you need? Plot the resulting graphs
for the three values of k.

4.2 Plot the propagation speed ṙs of the Strömgren sphere discussed in Sect. 4.1.1
relative to the speed of light (i.e. ṙs /c) in the time interval 0 < t/ts ≤ 2. Which
consequences do you see for the validity of the model at early time?

4.3 Study the dependence of the error on the time step for the differential equa-
tion (4.19), assuming the same initial conditions and parameters as in the example
discussed above. Start with an initial time step t = T /4, where T = 2π/ω is the
period of the damped oscillation and numerically integrate the initial value problem
over two periods. Iteratively decrease the time step by a factor of two until the relative
deviation of x(t = 2T ) from the analytic solution becomes less than 10−3 . List the
time steps and resulting deviations in a table for the symplectic Euler and RKN4
methods. Compare the time steps required for a relative error below 10−3 . Judging
from the order of the truncation error, what is your expectation?

4.2 Radial Fall

Eyewitnesses observed a bright flash followed by a huge explosion in a remote area


of the Siberian Taiga on June 30, 1908. The destruction was comparable to the blast
of a nuclear weapon (which did not exist at that time). More than a thousand square
kilometers of forest were flattened, but fortunately the devastated area was uninhab-
ited. Today it is known as the Tunguska event (named after the nearby river). What
could have caused such an enormous release of energy? The most likely explanation
4.2 Radial Fall 127

is an airburst of a medium sized asteroid several kilometers above the surface of


the Earth (in a competing hypothesis, a fragment of a comet is assumed) [11]. In
this case, the asteroid disintegrates completely and no impact crater is produced. A
sudden explosion can occur when the asteroid enters the lower atmosphere and is
rapidly heated by friction.
To estimate the energy released by the impact of an asteroid, let us consider the
simple case of radial infall toward the center of the Earth, i.e. in vertical direction
(depending on the origin, infalling asteroids typically follow a trajectory that is
inclined by an angle relative to the vertical). The equation of motion for free fall in
the gravitational potential of Earth is given by

G M⊕
r̈ = − (4.32)
r2
or in terms of the vertical height h = r − R⊕ above the surface:

G M⊕
ḧ = − (4.33)
(R⊕ + h)2

Assuming a total energy E = 0 (zero velocity at infinity), energy conservation


implies 
2G M⊕
v ≡ −ḣ = , (4.34)
R⊕ + h

where v is the infall velocity.


√ In particular, the impact velocity at the surface of Earth
(h = 0) is given by v0 = 2G M⊕ /R⊕ ≈ 11.2 km/s (use astropy.constants
to calculate this value). Even for an object following a non-radial trajectory, we
can use the kinetic energy E kin = 21 mv02 to estimate the typical energy gained by an
object of mass m falling into the gravity well of Earth. It is estimated that the asteroid
causing the Tunguska event was a solid body of at least 50 m diameter and a mass of
the order of 105 metric tons. With the radius and density from [11] a kinetic energy
of 2.3 × 1016 J is obtained. If such an energy is suddenly released in an explosion,
it corresponds to a TNT equivalent of about 5 megatons.11 The air burst that caused
devastation in Siberia in 1908 was probably even stronger because asteroids enter the
atmosphere of Earth at higher velocity (the additional energy stems from the orbital
motion around the Sun).
To compute the motion of an asteroid in Earth’s atmosphere, the following equa-
tion of motion has to be solved (we still assume one-dimensional motion in radial
direction):

11 Onekilogram of the chemical explosive TNT releases an energy of 4184 kJ. The most powerful
nuclear weapons ever built produced explosions of several 10 megatons.
128 4 Solving Differential Equations

G M⊕ 1
ḧ = − + ρair (h)CD Aḣ 2 (4.35)
(R⊕ + h) 2 2m

where A = π R 2 is the cross section. In addition to the gravity of Earth (first term
on the right-hand side), the asteroid experiences air resistance proportional to the
square of its velocity (this law applies to fast-moving objects for which the air flowing
around the object becomes turbulent). The asteroid is assumed to be spherical with
radius R. The drag coefficient for a spherical body is CD ≈ 0.5. The dependence of
the density of Earth’s atmosphere on altitude can be approximated by the barometric
height formula:
ρair (h) = 1.3 kg/m3 exp(−h/8.4 km) . (4.36)

Equation (4.35) is a non-linear differential equation of second order, which has to be


integrated numerically.
Let us first define basic parameters and initial data for the asteroid (based on [11]):

1 import numpy as np
2 import astropy.units as unit
3 from astropy.constants import G,M_earth,R_earth
4
5 # asteroid parameters
6 R = 34*unit.m # radius
7 V = (4*np.pi/3) * R**3 # volume
8 rho = 2.2e3*unit.kg/unit.m**3 # density
9 m = rho*V # mass
As start point for the numerical integration of the initial value problem, we choose
the height h 0 = 300 km. The internal space station (ISS) orbits Earth 400 km above
ground. The density of Earth’s atmosphere is virtually zero at such altitudes. As a
result, we can initially neglect the drag term and calculate the initial velocity v0 using
Eq. (4.34):
1 h0 = 300*unit.km
2 v0 = np.sqrt(2*G*M_earth/(R_earth + h0))
To apply our Runge-Kutta integrator from numkit, we need to express the second
derivative, ḧ, as function of t, h, and ḣ. In the following code, a Python function with
the formal arguments required by rkn4_step() and additional parameters for the
mass and radius of the asteroid is defined (see Sect. 4.1.2).
4.2 Radial Fall 129

1 from numkit import rkn4_step


2
3 # drag coefficient
4 c_drag = 0.5
5
6 # barometric height formula
7 def rho_air(h):
8 return 1.3*np.exp(-h/8.4e3)
9
10 # acceleration of the asteroid
11 def hddot(t, h, hdot, m, R):
12
13 # air resistance
14 F_drag =0.5*rho_air(h)*c_drag * np.pi*R**2 * hdot**2
15
16 # gravity at height h
17 g_h = G.value * M_earth.value / (R_earth.value + h)**2
18
19 return -g_h + F_drag/m
For the reasons outlined in Sect. 4.1, numerical integration is carried out in a unitless
representation (i.e. all variables are simple floats or arrays of floats). To extract
numerical values of constants from Astropy, we use the value attribute. Parameters
of the problem are defined in SI units or converted to SI units.
The integration of the initial value problem for the asteroid is executed by repeat-
edly calling rkn4_step() in a while loop until the height becomes non-positive.
We use a two-dimensional array called data to accumulate time, height, and velocity
from all integration steps:
20 # initial data
21 data = [[0, h0.to(unit.m).value, -v0.value]]
22
23 # initialization of loop variables
24 t, h, hdot = tuple(data[0])
25 print("Initial acceleration = {:.2f} m/s^2".
26 format(hddot(t, h, hdot, m.value, R.value)))
27
28 # time step
29 dt = 0.1
30
31 while h > 0:
32 h, hdot = rkn4_step(hddot, t, h, hdot, dt,
33 m.value, R.value)
34 t += dt
35 data = np.append(data, [[t, h, hdot]], axis=0)
130 4 Solving Differential Equations

The array data is initialized in line 21 with the initial values, which constitute the
first row of a two-dimensional array.12 Variables for the iteration of t, h, and ḣ are
defined in line 24, where tuple() is used to convert the row data[0] into a
tuple, which can be assigned to multiple variables in a single statement. The while
loop starting at line 31 calls rkn4_step() for the time step dt until h becomes
negative. Go through the arguments in lines 32–33 and check their counterparts in the
definition of rkn4_step(). After incrementing time, the updated variables h and
hdot are appended to the data array in line 35. We have already used the function
np.append() to append elements to a one-dimensional array (see Sect. 3.1.2).
In the case of a two-dimensional array, the syntax is more tricky because there are
different possibilities of joining multi-dimensional arrays. Here, we want to append
a new row. Similar to the initialization in line 21, the row elements have to be
collected in a two-dimensional array (as indicated by the double brackets) which is
then merged into the existing array. To that end, it is necessary to specify axis=0.
This axis is in the direction running through rows of an array (corresponding to the
first array index) and, thus, indicates that rows are to be appended to rows. If axis=0
is omitted, np.append() will flatten the resulting array, i.e. it is converted into a
one-dimensional array with all elements in a sequence (see for yourself what happens
to the data array without specifying the axis).13 Since we do not know the number of
time steps in advance, it is convenient to build the data array row by row. However,
this means that a new array is created and the complete data from the previous array
have to be copied for each iteration (the function’s name append is somewhat
misleading in this regard). For large arrays, this requires too much time and slows
down performance of the code. We will return to this issue in Sect. 4.4.
The output produced by the code listed so far is the acceleration ḧ at time t = 0:

Initial acceleration = -8.94 m/sˆ2

Its absolute value is close to Earth’s surface gravity g = 9.81 m/s2 . To see how the
motion progresses in time, we plot h(t) as a function of t and ḣ vs h:
36 import matplotlib.pyplot as plt
37
38 plt.figure(figsize=(12,4), dpi=100)
39
40 plt.subplot(121)
41 plt.plot(data[:,0], 1e-3*data[:,1])
42 plt.xlabel("$t$ [s]")
43 plt.ylabel("$h$ [km]" )
44
45 plt.subplot(122)

12 Strictly speaking, the expression with double brackets is a Python list, not a NumPy array. But

the list is implicitly converted into an array by np.append().


13 Arrays can be joined column-wise by using axis=1 provided that the second array is correctly

shaped. A simple example can be found in the notebook for this section.
4.2 Radial Fall 131

46 plt.plot(1e-3*data[:,1], -1e-3*data[:,2])
47 plt.xlim(h0.value+10,-10)
48 plt.xlabel("$h$ [km]")
49 plt.ylabel("-$\dot{h}$ [km/s]" )
50 plt.savefig("asteroid.pdf")
The plot particular variables we need to collect elements column-wise. In Python,
there is a simple mechanism for extracting multiple elements from an array, which
is called slicing. A slice of an array is equivalent to an index range. For example,
data[0,1:2] extracts the two elements with row index 0 and column index run-
ning from 1 to 2 (these elements are the height and velocity at time t = 0). To obtain
the data for the first 10 time steps, you would use the expression data[0:9,:].
If no start and end numbers are specified, the index simply runs through all possible
numbers, in this case, from 0 to 2. The first column of the data array, i.e. all time
values, is expressed as data[:,0]. This is the first argument of plt.plot()
in line 41. Can you identify the slices referring to other variables in the example
above? Furthermore, you might find it instructive to experiment with arbitrary slices
by printing them.
The plots shown in Fig. 4.5 are produced with the help of plt.subplot(). To
arrange two plots in a row, the left subplot is positioned with plt.subplot(121)
and the right subplot with plt.subplot(122), where the first digit indicates the
number of plots in vertical direction, the second is the number of plots in a row, and
the third enumerates the subplots starting from 1. At first glance, the numerically
computed solution might appear surprising. The height h(t) is nearly linear although
the asteroid is in free fall. The reason is that the free-fall velocities at radial distances
R⊕ + h 0 and R⊕ differ only by a small fraction since the initial height h 0 is small
compared to Earth’s radius R⊕ and the asteroid has gained most of its velocity in the
long infall phase prior to our arbitrarily chosen initial point t = 0. The range along
the horizontal axis of the right subplot (velocity vs height) is reverted (see line 47).
As a result, the height decreases from left to right corresponding to the progression

Fig. 4.5 Vertical motion of an asteroid of radius R = 34 m through Earth’s atmosphere. The left
plot shows the altitude h above ground as a function of time. The relation between the asteroid’s
velocity ḣ and h is shown in the right plot
132 4 Solving Differential Equations

Fig. 4.6 Heating rate caused by air resistance in Earth’s atmosphere

of time. As long as the asteroid is nearly in free fall, |ḣ| increases gradually, but then
the downward speed suddenly drops when the asteroid passes through the lower and
much denser layers of the atmosphere. This final phase lasts only a few seconds.
The heating due to air friction equals minus the rate at which kinetic energy is
dissipated per unit time. It can be calculated by multiplying the second term in the
expression for ḧ with the mass m and velocity ḣ (i.e. drag force times velocity):
51 def dissipation(h, hdot, m, R):
52 return -0.5*rho_air(h)*c_drag * np.pi*R**2 * hdot**3
The dissipation rate is plotted against height in Fig. 4.6 (the code producing this
plot can be found in the notebook for this chapter). In the lower layers of Earth’s
atmosphere, the asteroid decelerates and heating rises dramatically.
How much energy does the asteroid lose in total before it hits the ground? To
answer this question, we need to integrate the dissipation rate:
 t
CD A
E diss (t) = − ρair (h)ḣ 3 dt  . (4.37)
2 0

Since the data for h and ḣ are discrete, we can evaluate the function we want to
integrate only for t = nt (n = 0, 1, 2, . . .), but not for arbitrary t. For this reason,
we need to modify the numerical integration routines from Chap. 3. The following
version of the trapezoidal rule is applicable to an array of function values:
4.2 Radial Fall 133

53 def integr_trapez(y, h):


54 """
55 numerical integration of a function
56 given by discrete data
57
58 args: y - array of function values
59 h - spacing between x values
60
61 returns: approximate integral
62 """
63 return 0.5*h*(y[0] + 2*np.sum(y[1:-1]) + y[-1])
Do not confuse the formal argument h of this function (defined in the local name-
space) with the array h, which is the numerical solution of the initial value problem
for the asteroid.
As a simple test, let us integrate the function y = sin(x):
64 a, b = 0, np.pi/2
65 n = 10
66 x = np.linspace(a, b, n+1)
67 y = np.sin(x)
The array y contains the function values for the chosen subdivision of the integration
interval [0, π/2]. Now we can call integr_trapez() with this array and the
subinterval width (b-a)/n as arguments (compare to Sect. 3.2.2, where np.sin
is passed as argument):
68 integr_trapez(y, (b-a)/n)
The output is

0.9979429863543572

NumPy offers an equivalent library function for numerical integration using the
trapezoidal rule:
69 np.trapz(y, dx=(b-a)/n)
The result agrees within machine precision:

0.9979429863543573

To compute the fraction of kinetic energy that is dissipated by air resistance, we first
extract the data for h and ḣ by slicing the full data array. These data are used as
input to compute discrete values of the dissipation rate, which are in turn integrated
with dt as subinterval width:
70 energy_diss = integr_trapez(
71 dissipation(data[:,1], data[:,2],
134 4 Solving Differential Equations

72 m.value, R.value), dt) * unit.J


73 print("Fraction of dissipated energy = {:.2f} %".
74 format(100*energy_diss/energy_kin.to(unit.J)))
The ratio of the dissipated energy and the impact energy from above (both energies in
units of Joule) is printed as the final result. It turns out that about 5 % of the asteroid’s
energy are converted into heat (assuming that the asteroid makes it all the way to the
surface of Earth):

Fraction of dissipated energy = 5.55 %

In absolute terms, this is a sizable amount of energy. Since most of this energy is
released within a few seconds, it is plausible that the rapid heating may cause the
asteroid to disintegrate and burn up, as it likely happened in the Tunguska event.
In Exercise 4.5 you can investigate the much stronger impact of air resistance on
smaller objects called meteoroids.
Exercises

4.4 Rewrite Simpson’s rule (see Sect. 3.2.2) for an array of function values. Compute
the dissipated energy for the asteroid discussed in this section and compare to the
result obtained with the trapezoidal rule.
4.5 The influence of air resistance increases for smaller objects. Solve the differ-
ential equation (4.35) for a meteoroid of radius R = 25 cm (rocky objects of size
smaller than about one meter are called meteoroids rather than asteroids; when mete-
oroids enter Earth’s atmosphere, they become visible as meteors and their remnants
on ground are called meteorites).
1. Plot altitude and velocity of the meteoroid along with the asteroid data from the
example above. Interpret the differences.
2. Print the impact time and speed.
3. Estimate roughly when the falling meteoroid becomes a meteor (physically speak-
ing, a meteor is a meteoroid heated to high temperatures by friction).
4.6 The Falcon 9 rockets produced by SpaceX14 are famous for their spectacu-
lar landing via a so-called suicide burn. After deploying the payload at the target
orbit, the rocket performs a maneuver that brings it back into Earth’s atmosphere
where the atmospheric drag controls its maximum velocity. Assuming an equilib-
rium between gravity and air resistance, the downward acceleration ḧ approaches
zero and Eq. (4.35) implies that the terminal velocity for an object of mass m, cross-
section A, and drag coefficient cD is given by

2 gm
vmax = , (4.38)
ρcD A

14 See www.spacex.com/falcon9.
4.2 Radial Fall 135

where g is the gravitational acceleration. For instance, in the case of the first stage
of a Falcon 9 Full Thrust Block 5 rocket, the cross-section is nearly circular with a
radius of R = 1.83 m, a mass of m d = 27200 kg (dry mass without propellant), and
CD ≈ 0.5.
1. How fast would the rocket hit the sea if it descends to sea level (h = 0)?
2. For a safe touchdown, the velocity must be limited to a few meters per second,
which is much less than the terminal velocity resulting from atmospheric drag.
The rocket fires its thrusters to eliminate any lateral motion before it performs a
landing burn to decelerate in vertical direction. Since burning propellant reduces
not only the velocity but also the mass of the rocket, which in turn changes the
drag term, the computation is more complicated than in the case of an asteroid.
For a constant propellant burning rate b, the time-dependent rocket mass can be
written as
m(t) = m d + m p − b t (4.39)

where m p is the total propellant mass and m d the dry mass of the rocket. The
modified height equation for a thrust T produced by the rocket engines reads

G M⊕ 1 T
ḧ = − + ρair (h)CD Aḣ 2 − . (4.40)
(R⊕ + h)2 2 m(t) m(t)

Modify the Python function hddot() defined above such that the ignition thrust
term −T /m(t) is added for t ≥ tignite , where tignite is the ignition time. Include
additional parameters in the argument list.
3. Assume h 0 = 150 km and ḣ 0 = 2 km/s as initial data (t = 0). Define an array of
ignition times ranging from 50 to 70 s with 0.5 s spacing. For each ignition time,
solve Eq. (4.40) for T = 7.6 × 103 kN, m p = 3.0 × 104 kg, and b = 1480 kg/s.
Terminate integration if h = 0 is reached or the propellant is exhausted. Determine
the height at which the velocity ḣ switches signs from negative (descent) to
positive (ascend) as a function of tignite and plot the results.
4. To land the rocket safely, the ignition time must be adjusted such that ḣ is nearly
zero just at sea level. Estimate the optimal ignition time from the plot (it might be
helpful to use plt.xlim() to narrow down the range) and interpolate between
the nearest date points. Determine the touchdown time, speed, and remaining
mass of propellant for the interpolated ignition time.

4.7 How long does it take to fall into a black hole? Well, the answer depends quite
literally on the point of view. Suppose a spaceship is plunging from an initial radial
distance r0 toward a black hole of mass M. For simplicity, we assume that the
spaceship has zero initial velocity and is accelerated by the gravitational pull of the
black hole. We also neglect any orbital angular momentum. Such initial conditions are
unrealistic, but our assumptions will suffice for argument’s sake. The radial position
r at time t is determined by the following differential equation [12]:
136 4 Solving Differential Equations
    
dr RS RS RS 1/2 RS −1/2
= −c 1 − − 1− , (4.41)
dt r r r0 r0

where c is the speed of light and RS = 2G M/c2 is the Schwarzschild radius of the
black hole. In this exercise, we will consider two scenarios: (a) a stellar black hole of
mass M = 10 M and (b) the supermassive black hole at the center of the Milkyway
with M ≈ 4 · 106 M . As you can see from the right-hand side of the Eq. (4.41), RS
plays a crucial role. Solve the initial value problem for r0 = 100RS and plot r (t).
You will find that the spaceship never crosses the sphere with radius r = RS , which
is called the event horizon of the black hole, but appears to hover just above the
horizon for eternity.
So is there actually nothing to be feared by someone on board of the spaceship,
except being captured in the vicinity of the black hole? In relativity, the progression
of time (i.e. the time interval measured by a clock) depends on the observer’s frame
of reference. The solution following from Eq. (4.41) is what a distant observer far
away from the black hole will witness, where the effects of gravity are negligible. For
the crew of the spaceship, however, a dramatically different chain of events unfolds.
What they read on the starship’s clocks is called proper time τ and, for the crew, the
rate of change of radial position is given by15
 1/2
dr RS RS
= −c − . (4.42)
dτ r r0

Solve this equation for the same initial conditions as above. At which time would
the spaceship cross the event horizon of the stellar and the supermassive black hole?
In the case of the stellar black hole the spaceship will be torn apart by tidal forces
even before it reaches the horizon (see Exercise 2.12). Putting aside tidal effects, how
long will it take the spaceship until it finally gets crushed by the singularity at r = 0?
The completely different outcome for the crew of the spaceship is a consequence of
gravitational time dilation, which becomes extreme in the case of a black hole.

4.3 Orbital Mechanics

The motion of a planet and a star or two stars around the common center of mass (in
astronomy also called barycenter) is governed by Kepler’s laws. The properties of
the orbits are determined by integrals of motion such as the total energy and orbital
angular momentum. Alternatively, we can solve the equations of motion directly.
For systems composed of more than two bodies interacting with each other, there
is no other way than numerical integration. To begin with, we will apply different

15 In the non-relativistic limit, τ  t and you can derive Eq. (4.42) from energy conservation E kin +
E pot = 0.
4.3 Orbital Mechanics 137

numerical methods to test whether Keplerian orbits can be reproduced by solving


the initial value problem for two bodies numerically.
As an example, let us compute the orbits of the binary stars Sirius A and B (see
also Sect. 3.1.1). In the center-of-mass reference frame (i.e. the center of mass is at
the origin), we have position vectors

M2 M1
r1 = − d, r2 = d, (4.43)
M1 + M2 M1 + M2

where d = r2 − r1 is the distance vector between the two stars. The accelerations
are given by
F12 F21
r̈1 = , r̈2 = , (4.44)
M1 M2

with the gravitational force

G M1 M2
F12 = −F21 = d. (4.45)
d3
To define initial conditions, we make use of the vis-viva equation:
 
2 1
v = G (M1 + M2 )
2
− , (4.46)
d a

where v is the modulus of the relative velocity ḋ, G is Newton’s constant, and a
is the semi-major axis of the motion of the distance vector d(t). Remember that
the two-body problem can be reduced to a one-body problem for a body of mass
μ = M1 M2 /(M1 + M2 ) moving along an elliptic orbit given by d(t). At the points
of minimal and maximal distance, the velocity vector ḋ is perpendicular to d. At the
periastron, where the two stars are closest to each other, we have
 
M2
r1 (0) = dp , 0 , 0 , (4.47)
M1 + M2
 
M1
r2 (0) = − dp , 0 , 0 , (4.48)
M1 + M2

assuming that the major axes of the ellipses are aligned with the x-axis. For ellipses of
eccentricity e, the periastron distance is given by dp = a(1 − e) and the correspond-
ing relative velocity, vp , is obtained by substituting dp into Eq. (4.46). By orienting
the z-axis perpendicular to the orbital plane, the orbital velocities at the periastron
can be expressed as
138 4 Solving Differential Equations
 
M2
v1 (0) ≡ ṙ1 (0) = 0 , − vp , 0 , (4.49)
M1 + M2
 
M1
v2 (0) ≡ ṙ2 (0) = 0 , vp , 0 . (4.50)
M1 + M2

This completes the initial value problem for the two stars.
To proceed with Python, we first define masses and orbitial parameters:
1 import numpy as np
2 from scipy.constants import G,year,au
3 from astropy.constants import M_sun
4
5 M1 = 2.06*M_sun.value # mass of Sirius A
6 M2 = 1.02*M_sun.value # mass of Sirius B
7
8 a = 2.64*7.4957*au # semi-major axis
9 e = 0.5914
Sirius B is a white dwarf of about one solar mass and Sirius A a more massive
main-sequence star. In line 8, the semi-major axis a ≈ 20 AU is calculated from the
distance of the star system from Earth and its angular size. The orbital eccentricity
of about 0.6 indicates a pronounced elliptical shape. The orbital period follows from
Kepler’s third law:
1 T = 2*np.pi * (G*(M1 + M2))**(-1/2) * a**(3/2)
2
3 print("Orbital period = {:.1f} yr".format(T/year))
Since Sirius A and B orbit each other at relatively large distance, they need years to
complete one orbital revolution:

Orbital period = 50.2 yr

So far, we have solved differential equations for a single function (e.g. the displace-
ment of an oscillator in Sect. 4.1 and the radial coordinate in Sect. 4.2). In the case
of the two-body problem, we are dealing with a system of coupled differential equa-
tions (4.44) for the vector functions r1 (t) and r2 (t). By reformulating the equations
of motion as a system of first-order differential equations,

G M2 G M1
v̇1 = (r2 − r1 ) , v̇2 = (r1 − r2 ) , (4.51)
|r2 − r1 |3 |r2 − r1 |3
ṙ1 = v1 , ṙ2 = v2 , (4.52)

it is straightforward to generalize the forward Euler method described in Sect. 4.1.2.


First we set the integration interval in units of the orbital period and the number of
time steps. Then we initialize arrays for the coordinates and velocity components (the
4.3 Orbital Mechanics 139

orientation of the coordinate frame is chosen such that the orbits are in the x y-plane
and z-coordinates can be ignored):
4 n_rev = 3 # number of revolutions
5 n = n_rev*500 # number of time steps
6 dt = n_rev*T/n # time step
7 t = np.arange(0, (n+1)*dt, dt)
8
9 # data arrays for coordinates
10 x1 = np.zeros(n+1)
11 y1 = np.zeros(n+1)
12 x2 = np.zeros(n+1)
13 y2 = np.zeros(n+1)
14
15 # data arrays for velocity components
16 vx1 = np.zeros(n+1)
17 vy1 = np.zeros(n+1)
18 vx2 = np.zeros(n+1)
19 vy2 = np.zeros(n+1)
Before proceeding with the numerical integration, we need to assign the initial con-
ditions (4.47) to (4.50) to the first elements of the data arrays:
20 # periastron distance and relative velocity
21 d = a*(1 + e)
22 v = np.sqrt(G*(M1 + M2)*(2/d - 1/a)) # vis-viva eq.
23
24 x1[0], y1[0] = d*M2/(M1 + M2), 0
25 x2[0], y2[0] = -d*M1/(M1 + M2), 0
26
27 vx1[0], vy1[0] = 0, -v*M2/(M1 + M2)
28 vx2[0], vy2[0] = 0, v*M1/(M1 + M2)
Time integration is implemented in the following for loop through all time steps:
29 alpha = G*M1*M2
30
31 for i in range(n):
32
33 delta_x = x2[i] - x1[i]
34 delta_y = y2[i] - y1[i]
35
36 # third power of distance
37 d3 = (delta_x**2 + delta_y**2)**(3/2)
38
39 # force components
40 Fx = alpha*delta_x/d3
140 4 Solving Differential Equations

41 Fy = alpha*delta_y/d3
42
43 # forward Euler velocity updates
44 vx1[i+1] = vx1[i] + Fx*dt/M1
45 vy1[i+1] = vy1[i] + Fy*dt/M1
46 vx2[i+1] = vx2[i] - Fx*dt/M2
47 vy2[i+1] = vy2[i] - Fy*dt/M2
48
49 # forward Euler position updates
50 x1[i+1] = x1[i] + vx1[i]*dt
51 y1[i+1] = y1[i] + vy1[i]*dt
52 x2[i+1] = x2[i] + vx2[i]*dt
53 y2[i+1] = y2[i] + vy2[i]*dt

Fig. 4.7 Numerical solution of the two-body problem for the binary stars Sirius A and B computed
with the forward Euler method. The center of mass is located at the origin (black cross)

Figure 4.7 shows the resulting orbits (the Python code producing the plot is listed
below). Although the shape resembles an ellipse, they solution is clearly not correct.
The two stars are gradually drifting outwards and their apastrons (most distant points)
are not at the x-axis. As a result, there are no closed orbits. This is in contradiction
with the analytic solution of the two-body problem.
54 import matplotlib.pyplot as plt
55 %matplotlib inline
56
57 fig = plt.figure(figsize=(6, 6*35/55), dpi=100)
58
59 plt.plot([0], [0], ’+k’) # center of mass
60 plt.plot(x1/au, y1/au, color=’red’, label=’Sirius A’)
4.3 Orbital Mechanics 141

61 plt.plot(x2/au, y2/au, color=’blue’, label=’Sirius B’)


62
63 plt.xlabel("$x$ [AU]")
64 plt.xlim(-20,35)
65 plt.ylabel("$y$ [AU]")
66 plt.ylim(-17.5,17.5)
67 plt.legend(loc=’upper left’)
68 plt.savefig("sirius_forward.pdf")
Of course, you know already from Sect. 4.1.2 that the forward Euler method is not
suitable to solve dynamical problems. The numerical solution changes remarkably
with the following simple modification of lines 50–53:
50 x1[i+1] = x1[i] + vx1[i+1]*dt
51 y1[i+1] = y1[i] + vy1[i+1]*dt
52 x2[i+1] = x2[i] + vx2[i+1]*dt
53 y2[i+1] = y2[i] + vy2[i+1]*dt
After re-running the solver, two closed elliptical orbits are obtained (see Fig. 4.8).
Even after three revolutions, there is no noticeable drift. Based on what you learned
in Sect. 4.1.2, you should be able to explain how this comes about (a hint is given in
the caption of the figure).
How can we further improve the accuracy of the solution? Surely, by using
a higher-order scheme. Since the implementation of a Runge-Kutta scheme for
systems of differential equations is a laborious task, we make use of library
functions for solving initial value problems from SciPy. The wrapper function
scipy.integrate.solve_ivp() allows you to solve any system of first-
order ODEs (higher-order systems can be reformulated as first-order systems) with

Fig. 4.8 Same as in Fig. 4.7, but computed with a semi-implicit scheme
142 4 Solving Differential Equations

different numerical integrators.16 Before applying this function, you need to famil-
iarize yourself with the notion of a state vector. Suppose we have a system of ODEs
for N functions sn (t), where n ∈ [1, N ]:

ṡ1 = f 1 (t, s1 , . . . , s N ) ,
..
. (4.53)

ṡ N = f N (t, s1 , . . . , s N ) . (4.54)

This can be written in vector notation as

ṡ = f(t, s) , (4.55)

where s = (s1 , . . . , s N ) and f = ( f 1 , . . . , f N ). Equation (4.55) allows us to compute


the rate of change ṡ for any given state s at time t. In the case of the two-body problem,
we can combine Eqs. (4.51), (4.52) into a single equation for an eight-dimensional
state vector (again ignoring z-components):
⎛ ⎞ ⎛ ⎞
x1 v1x
⎜ y1 ⎟ ⎜ v1y ⎟
⎜ ⎟ ⎜ ⎟
⎜ x2 ⎟ ⎜ v2x ⎟
⎜ ⎟ ⎜ ⎟
s = ⎜ . ⎟ and f(t, s) = ⎜ .. ⎟ (4.56)
⎜ .. ⎟ ⎜ . ⎟
⎜ ⎟ ⎜ ⎟
⎝v2x ⎠ ⎝−F12x /M2 ⎠
v2y −F12y /M2

It does not matter how the variables are ordered in the state vector, but it is helpful
to use mnemonic ordering such that the index n can be easily associated with the
corresponding variable. Generally, each component f n of the right-hand side of
Eq. (4.55) may depend on all or any subset of the variables s1 , . . . , s N . For example,
f 1 is only a function of s5 = v1x , while f 8 depends on s1 = x1 , s2 = y1 , s3 = x2 , and
s4 = y2 (see definition of the gravitational force (4.45)).
In Python, state vectors can be defined as NumPy arrays. For example, the initial
state s(0) is given by the following array:
54 from scipy.integrate import solve_ivp
55
56 init_state = np.array([ x1[0], y1[0], x2[0], y2[0],
57 vx1[0], vy1[0], vx2[0], vy2[0]])
The array elements (initial positions and velocities of the stars) are defined in lines
24–28. To apply solve_ivp(), we need to define a Python function that evaluates
the right-hand side of Eq. (4.55):

16 See docs.scipy.org/doc/scipy/reference/generated/scipy.integrate.solve_ivp.html.
4.3 Orbital Mechanics 143

58 def state_derv(t, state):


59 alpha = G*M1*M2
60
61 delta_x = state[2] - state[0] # x2 - x1
62 delta_y = state[3] - state[1] # y2 - y1
63
64 # third power of distance
65 d3 = (delta_x**2 + delta_y**2)**(3/2)
66
67 # force components
68 Fx = alpha*delta_x/d3
69 Fy = alpha*delta_y/d3
70
71 return np.array([state[4], state[5], state[6], state[7],
72 Fx/M1, Fy/M1, -Fx/M2, -Fy/M2])
The elements returned by this function are the components of the vector f(t, s) defined
by Eq. (4.56).
The following call of solve_ivp() computes the solution for the time interval
[0, 3T ] starting from init_state:
73 tmp = solve_ivp(state_derv, (0,3*T), init_state,
74 dense_output=True)
75 data = tmp.sol(t)
The keyword argument dense_output=True indicates that the solve_ivp()
returns a Python function with the name sol.17 This function can be used to com-
pute the values of the solution for any given array of time values via polynomial
interpolation. This is what happens in line 75, where the solution is evaluated for the
array t defined above (tmp is a temporary object containing everything returned by
solve_ivp(), including sol()).
The resulting data can be plotted by means of array slicing, where the row index
corresponds to the index of state and the column index is running through values
at subsequent instants:
76 fig = plt.figure(figsize=(6, 6*25/35), dpi=100)
77
78 plt.plot([0], [0], ’+k’) # center of mass
79 plt.plot(data[0,:]/au, data[1,:]/au,
80 color=’red’, label=’Sirius A’)
81 plt.plot(data[2,:]/au, data[3,:]/au,
82 color=’blue’, label=’Sirius B’)
83
84 plt.xlabel("$x$ [AU]")
85 plt.xlim(-12.5,22.5)
86 plt.ylabel("$y$ [AU]")
87 plt.ylim(-12.5,12.5)

17 Since a function in Python is an object, a function can be returned by a another function.


144 4 Solving Differential Equations

88 plt.legend(loc=’upper left’)
89 plt.savefig("sirius_scipy.pdf")
The plot is shown in Fig. 4.9. It turns out the orbits are note quite elliptical. The
semi-major axes is slowly shrinking. You might find this surprising, as the docu-
mentation shows that solve_ivp() applies a higher-order Runge-Kutta scheme
by default. However, the time step and error tolerances are adjusted such that the
integrator computes the solution efficiently with a moderate number of time steps. It
is also important to keep in mind that Runge-Kutta methods are not symplectic (see
Sect. 4.1.2). Therefore, the error can grow with time. The lesson to be learned here is
that numerical library functions have to be used with care. They can be convenient
and may offer more sophisticated methods than what you would typically program
yourself. But to some degree you need to be aware of their inner workings and tuning
parameters to avoid results that do not meet your expectation. In Exercise 4.8, you
can further explore solve_ivp() and learn how to improve the accuracy of the
orbits of Sirius A and B.
To conclude our discussion of orbital mechanics, we will solve a special case of
the three-body problem, where two objects are in a close, inner orbit. Together with
a third, more distant object, they follows an outer orbit around the common center
of mass. Such a configuration is found in the triple star system Beta Persei, which is
also known as Algol. Algol Aa1 and Aa2 constitute an eclipsing binary with a period

Fig. 4.9 Same as in Fig. 4.8, but computed with the initial value problem solver from SciPy (fifth-
order Runge-Kutta scheme with fourth-order error estimator)
4.3 Orbital Mechanics 145

of less than three days.18 This binary and a third star, designated Ab, revolve around
each other over a period of 680 days. Here are the orbital parameters of the Algol
system:
1 from scipy.constants import day
2
3 M1 = 3.17*M_sun.value # mass of Algol Aa1
4 M2 = 0.70*M_sun.value # mass of Algol Aa2
5 M3 = 1.76*M_sun.value # mass of Algol Ab
6
7 # inner orbit (Aa1 and Aa2)
8 T12 = 2.867*day
9 e12 = 0
10
11 # outer orbit (Aa and Ab)
12 T = 680.2*day
13 e = 0.227
Since the orbital periods are known with higher precision, we compute the semi-
major axes of the inner and outer orbits using Kepler’s third law, assuming that the
binary Aa and the star Ab can be treated as a two-body system with masses M1 + M2
and M3 . For the inner orbit (Aa1 and Aa2), the third star can be ignored, similar to
the Earth-Moon system and the Sun:
14 from scipy.constants import day
15
16 a12 = (T12/(2*np.pi))**(2/3) * (G*(M1 + M2))**(1/3)
17 a = (T/(2*np.pi))**(2/3) * (G*(M1 + M2 + M3))**(1/3)
18
19 print("Inner semi-major axis = {:.2e} AU".format(a12/au))
20 print("Outer semi-major axis = {:.2f} AU".format(a/au))
As expected, the size of the inner orbit is much smaller than the outer orbit (less ten
10 million km vs roughly the distance from the Sun to Mars).

Inner semi-major axis = 6.20e-02 AU


Outer semi-major axis = 2.69 AU

This allows us to define approximate initial conditions. First, we define periastron


positions and velocities of Aa1 and Aa2 analogous to Sirius:
21 M12 = M1 + M2
22 d12 = a12*(1 - e12)
23 v12 = np.sqrt(G*M12*(2/d12 - 1/a12))
24
25 x1, y1 = d12*M2/M12, 0

18 The term eclipsing binary means that the orbital plane is nearly parallel to the direction of the line

of sight from Earth. For such a configuration, one star periodically eclipses the other star, resulting
in a characteristic variation of the brightness observed on Earth.
146 4 Solving Differential Equations

26 x2, y2 = -d12*M1/M12, 0
27
28 vx1, vy1 = 0, -v12*M2/M12
29 vx2, vy2 = 0, v12*M1/M12
For the next step, think of Aa1 and Aa2 as a single object of total mass M1 + M2
(variable M12) positioned at the binary’s center of mass. By treating the binary and
Ab in turn as a two-body system, we have
30 d = a*(1 - e)
31 v = np.sqrt(G*(M12 + M3)*(2/d - 1/a))
32
33 x1 += d*M3/(M12 + M3)
34 x2 += d*M3/(M12 + M3)
35
36 x3, y3 = -d*M12/(M12 + M3), 0
37
38 vy1 -= v*M3/(M12 + M3)
39 vy2 -= v*M3/(M12 + M3)
40
41 vx3, vy3 = 0, v*M12/(M12 + M3)
In lines 33–34, the x- and y-coordinates are shifted from the center-of-mass frame of
Aa1 and Aa2 to the center-of-mass frame for all three stars. The Keplerian velocities
at the periastron of the outer orbit are added to the velocities from above (lines 38–
39), while Ab moves only with the outer orbital velocity (line 41). As you can see,
two further assumptions are applied here. First of all, all three stars are assumed to
be simultaneously at their periastrons at time t = 0. Generally, periastrons of the
inner and outer orbits do not coincide. Second, the plane of the outer orbit of Algol is
inclined relative to the plane of the inner orbit. As a result, it would be necessary to
treat the motion of the three stars in three-dimensional space. For simplicity’s sake,
we will ignore this and pretend that the orbits are co-planar.
We solve the equations of motion for the full three-body interactions, i.e. the
resulting force acting on each star is given by the sum of the gravitational fields of
the other two stars. For example, the equation of motion of Algol Aa1 reads

F12 + F23
r̈1 = , (4.57)
M1

where
G M1 M2 G M1 M3
F12 = 3
d12 , F13 = 3
d13 , (4.58)
d12 d13

and the displacements vectors are given by

d12 = d2 − d1 , d13 = d3 − d1 . (4.59)


4.3 Orbital Mechanics 147

Analogous equations apply to Algol Aa2 and Ab. In terms of the state vector of the
system, which has twelve components (six positional coordinates and six velocity
components), these equations can be implemented as follows.
42 def state_derv(t, state):
43 alpha = G*M1*M2
44 beta = G*M1*M3
45 gamma = G*M2*M3
46
47 delta12_x = state[2] - state[0] # x2 - x1
48 delta12_y = state[3] - state[1] # y2 - y1
49
50 delta13_x = state[4] - state[0] # x3 - x1
51 delta13_y = state[5] - state[1] # y3 - y1
52
53 delta23_x = state[4] - state[2] # x3 - x2
54 delta23_y = state[5] - state[3] # y3 - y2
55
56 # force components
57 F12x = alpha*delta12_x/(delta12_x**2 + delta12_y**2)**(3/2)
58 F12y = alpha*delta12_y/(delta12_x**2 + delta12_y**2)**(3/2)
59
60 F13x = beta*delta13_x/(delta13_x**2 + delta13_y**2)**(3/2)
61 F13y = beta*delta13_y/(delta13_x**2 + delta13_y**2)**(3/2)
62
63 F23x = gamma*delta23_x/(delta23_x**2 + delta23_y**2)**(3/2)
64 F23y = gamma*delta23_y/(delta23_x**2 + delta23_y**2)**(3/2)
65
66 return np.array([state[6], state[7],
67 state[8], state[9],
68 state[10], state[11],
69 ( F12x + F13x)/M1, ( F12y + F13y)/M1,
70 (-F12x + F23x)/M2, (-F12y + F23y)/M2,
71 (-F13x - F23x)/M3, (-F13y - F23y)/M3])
It is left as an exercise to define the initial state vector for the system and to compute
and plot the solution using solve_ivp().19 Figure 4.10 shows the resulting orbits
for the time interval [0, 0.5T ]. Algol Aa1 and Aa2 nearly move like a single object
along a Kepler ellipse. In a close-up view the two stars revolve on much smaller
orbits around their center of mass (see Fig. 4.11). Combined with the center-of-mass
motion, the stars follow helix-like paths. Since the period of the binary’s inner orbit
is much shorter, a sufficiently small time step has to be chosen (for the orbits shown

19 If this is too time consuming for you, the complete code can be found in the notebook and source

files for this chapter.


148 4 Solving Differential Equations

Fig. 4.10 Orbits of three stars similar to the Algol system

in Figs. 4.10 and 4.11 the time step is 0.1 d). This results in a large number of time
steps if the equations of motion are integrated over a time interval comparable to the
period of the outer orbit. Disparate time scales are a common problem in numerical
computation. Often it is not feasible to follow the evolution of a system from the
smallest to the largest timescales. In the case of the Algol system, for example, an
approximative solution would be to solve the two-body problem for the inner and
outer orbits separately. In order to do this, how would you modify the code listed
above?
Exercises

4.8 Compare different solvers in solve_ivp() for the orbits of Sirius A and
B. The solver can be specified with they keyword argument method. Moreover
investigate the impact of the relative tolerance rtol. See the online documentation
for details.

4.9 Compute the motion of a hypothetical planet in the Sirius system. Apply the
test particle approximation to the planet, i.e. neglect the gravity of the planet in
the equations of motion of the stars, while the planets’s motion is governed by the
gravitational forces exerted by Sirius A and B.
(a) Determine initial data from the apastron of a Kepler ellipse of eccentricity =
0.3, assuming a single star of mass M1 + M2 ≈ 3.08 M at the center of mass of
binary. Under which conditions do you expect this to be a good approximation?
Solve the initial value problem for different ratios of the initial distance of the
4.3 Orbital Mechanics 149

Fig. 4.11 Close-up view of


the inner orbits in the
center-of-mass frame of the
Algol system

planet from the barycenter and the semi-major axis of the binary star (a ≈
20 AU).20 In other words, consider cases where the size of the planetary orbit
is comparable to the distance between the two stars and where the planet moves
far away from the binary. How does binary affect the planet’s orbit over several
periods and in which respect does it differ from a Keplerian orbit?
(b) Now assume that the planet follows a close circular orbit around Sirius A (i.e.
the radius of the orbit is small compared to the separation of the binary). Which
approximation can be applied to initialize the orbit in this case? Successively
increase the orbital radius from about 0.1 AU to larger values. At which distance
from Sirius A destabilizes the planetary orbit within several revolutions (i.e.
begins to deviate substantially from the initial elliptical shape)?

4.10 In general, the three-body problem cannot be solved analytically. In many cases
motions are chaotic, while stable configurations are rare. Consider a system of three
stars, each with a mass of 1 M . Intialize the spatial coordinates and velocities of

20 Another parameter is the relative orientation of the major axes of the orbit of the binary and the
planet’s orbit. You can also vary this parameter if you are interested in its influence.
150 4 Solving Differential Equations

each star with uniformly distributed random numbers. For the coordinates x, y, and z,
generate random numbers in the interval [−1 AU, 1 AU] using the NumPy function
random.uniform(), whose first and second argument are the endpoints of the
interval from which a random number is to be drawn. By setting the third argument
to 3, the function returns three random numbers for the three coordinates. In the
same way, generate random velocity
√ components vx , v y , and vz within the interval
[−vmax , vmax ], where vmax = G M /0.01 AU is the orbital velocity around a solar
mass at a distance of 0.01 AU (this is 10 times the orbital velocity of Earth around
the Sun).
(a) Solve the initial value problem for a time interval of at least 10 yr. Plot the
pairwise distances d12 , d13 , and d23 between the stars as function of time. Repeat
the procedure for several randomly chosen initial conditions and interpret your
results. How can you identify bound orbits? (Think about the time behaviour of
the distance between two stars in a two-body system.)
(b) The displacement vectors d12 , d13 , and d23 form a triangle with the three stars
at the vertices. Calculate the time-dependent internal angles of the triangle by
applying the law of cosines. If the stars are eclipsing (i.e. they are aligned along
a line), the triangle will degenerate into a line. As a result, you can detect
eclipses by tracking the time evolution of the internal angles. If the configu-
ration approaches an eclipse, the smallest and largest angles will be close to 0
and 180◦ , respectively. Can you identify such events in your sample?21
(c) You can draw a triangle that is similar to the displacement tri-
angle (i.e. a triangle with identical internal angles) by utilizing Polygon()
from matplotlib.patches. Position the first star at the origin, align d12
with the horizontal coordinate axis of the plot and place the third vertex of the
triangle such that angles are preserved. What can you deduce from the time
evolution of the shape of the triangle? (Optionally, you can return to this exer-
cise after reading the next section. You will then be able to illustrate the time
evolution of the three-body system by animating the triangle.)
(d) If you want to optimize the performance of your code to compute larger samples,
you can follow the instructions in Appendix B.2.

21 The three-body problem and its rich phenomenology is intriguing for astrophysicists and mathe-

maticians alike. For example, properties of the displacement triangles can be analyzed in an abstract
shape space, which can be mapped to a sphere, the so-called shape sphere. The evolution of any
three-body system is described by a curve on this sphere. Eclipses live on the sphere’s equator. See
[13] and references therein.

You might also like