A physicist view on gRPC — Simulating the Sun-Earth-Moon three-body system

Gil-Arnaud Coche
16 min readMar 21, 2024

--

Leveraging distributed computing to simulate celestial bodies

All codes are available on GitHub.

Amaterasu, Sun Goddess in Shintoism, with the Moon and the Earth, connected in a gRPC network (Dall.e)

It is very frequent, in trading system to define proprietary protocols and implement them using proprietary RPC frameworks.

Recently, I was told about gRPC. And upon hearing about gRPC,

(i) I was surprised that the trading systems I worked on until not long ago, not so many were leveraging the technology;

(ii) I just wanted to implement something to try it out.

Why? What is gRPC? If you ask my new best coding partner (chatGPT), he’ll say this

gRPC is a high-performance, open-source framework for RPC (Remote Procedure Call) developed by Google. It enables client and server applications to communicate transparently and efficiently across different environments and languages.

In a nutshell, gRPC allows you to write applications across multiple processes as it provides the communication protocols as well as the server/client codes.

There is often a lot of boilerplate code that you can avoid with the right abstractions and that is what gRPC is: a great abtraction of RPC’s.

For those of you who have read some of my former publications, you may be aware that Rust has taken a lot of my interest lately. There are crates to use gRPC with Rust. I will probably be using these crates in the near future but as they are non official, and to get introduced to gRPC, I thought I would take the opportunity to gain some programming experience in another language that is fully supported by gRPC dev’ teams.

Since Golang is available with an official support in gRPC and I have always been found of the language, I threw myself in without looking back.

As for the project, and as the name of this blog states, it had to be about some form of mathematical modeling. Recently, I had been publishing a lot about Quant Finance, so my heart felt like doing something else.

Thus, a little out of nowhere, I figured I would simulate the whole Solar System: one gRPC client per celestial body. I was thrilled about that idea.

However, while working on the project, I resized it as I realized that I had to carefully design server and clients using gRPC, but also the Ordinary Differential Equation solver. The latter ended up taking more time than initially planned. So I decided to focus on the classical three body system made of the Sun, the Earth and the Moon. I can always come back to the full Solar System later on.
(Hint… Hint…)

So we’ll build a gRPC system with a central server that shares informations amongst clients: each of these representing either the Sun, the Earth or the Moon.

This project was very pleasant as it threw me back in space and the wonderful mechanical physics that goes with it. I should say though, this post is mainly about gRPC rather than celestial mechanics. As you shall see, all I have done here is coherent from a physcial stand point, but the main take away is that you can solve the Sun-Earth-Moon three body system using gRPC. And I find that very cool…

I even think that it could make a lot of sense to systematically use gRPC for complex physical calculations. gRPC offers a very convenient way to define whatever celestial system you like!

First we’ll go through the maths: you don’t really need a proper experience in mechanics to understand the rough idea — if you disagree with me on that point, I am sorry, and please tell me how I failed to make that material accessible.

Second, I’ll get into the details of the implementation. Although I used chatGPT (obviously) to learn about gRPC and debug my Golang doodles, the codes are really mine as I don’t think one can properly learn anything without doing things themselves.

Third, we’ll have a look at the results. Initially, I was having really crazy results and thought something was wrong with my programs… Turns out it was the initial conditions! They are extremely important. It is now obvious why, but initially, I really thought I could just throw some rough numbers, but it turns out that I actually had to be very careful with these initial parameters!

行くぞ!

Background Maths

Let’s go back to undergrad physics and write down the equations of motion for the three body system made of the Sun, the Earth and the Moon.

Center of Mass is the center of the simulation

On the figure below, a non-scaled diagram of the Sun (point S), the Earth (point E) and the Moon (point M).

Geometry of the Sun, Earth & Moon system

Each of these points has a mass and classical mechanics defines the center of mass G of these points with the equation

assuming that O is a fixed point in space. Now if we assume that i (either S, E, M) exerts a force on j (either S, E, M, but not i) and we write it

we could also draw a more detailed diagram with the interactions between the bodies as shown on the figure below.

Forces exerted on all bodies in the Sun, Earth & Moon system

The 2nd law of Newton applied to each of the bodies gives

In English, the 2nd law of Newton states that the rate of change of velocity of each of the bodies (Sun, Earth or Moon) is equal to the forces applied on each of them.

And thus, by taking the second order derivatives in the definition of the center of mass, we find that the second order derivative of OG is

The 3rd law of Newton now states that necessarily for every couples of i and j such that i is not equal to j

In English, the force exerted by the Moon on the Earth is exactly the opposite of the force exerted by the Earth on the Moon. And his statement remains true for any pair in the three body system.

The result of all these statements is that the right hand side of the equation on the second order derivative of OG is equal to 0

In English, the center of Gravity has uniform motion in a right line: it never accelerates.

And that is why we can set the center of mass as the center of the frame used in the calculations — I am voluntarily not mentionning Galilean Frames but for those of you who are familiar with these concepts, that is what the center of mass G defines.

Equations of Motion

Now that we have our center of frame, let us delve deeper into the geometry and state the equations we will use to simulate the motion of each body.

Let’s start with the position vectors of each celestial body. We write them:

which can be shown on the diagram

Geometry and forces of one of the bodies i with the center of Mass G.

where i is any of S, E or M. If we introduce the vector

that represents the position of j relative to i, according to Newton’s law of universal gravitation, the gravitational pull of j on i is

which implies that the 2nd of Newton on body i may be stated as

And introducing a cartesian frame that is centered in G, we find that the equations of motions are

where xᵢ and yᵢ are the coordinates of i.

Dimensionless Equations of Motion

A common practice in physics computations, is to render equations dimensionless to avoid over- or underflows. Physics equations can have quantities with very great values (like in this case) or very little values (like in molecular dynamics or quantum physics).

Here G is extremly small as G = 6,67430e−11 and distances are extremely big as the Sun-Earth distance is about 1.5e+08 kilometers… Some of you may notice that there seems to be a compensation between G and the distances. So yes maybe it is not necessary to rely on dimensionless equations, but it has the merit to remove that worry, so let’s do it anyway.

To get to the dimensionless equation, one has to introduce scaling parameters and dimensionless variables. In our Sun-Earth-Moon three body system, I chose to introduce the scaling parameters

where D is the minimal distance between the Sun and the Earth and T is of the order of magnitude of the orbital period of the Earth around the Sun — there should be a for the full formula.

I then naturally chose the dimensionless variables as

If you use the scaled masses

you eventually find that the dimensionless equations are (I dropped the ~’s for clarity) in cartesian coordinates

or more formally (dropping this time the i’s for clarity)

Equations of motion as a Hamiltonian system

Before moving on to the implementation section, it will be convenient to state the above equation in an even more reduced form using the vector

Indeed, with the right definition for the function f, the equations of motion become

(assuming that all other bodies’ positions are paremeters of f).

Implementing the system

Let’s get real now, shall we? 🤓

What we want — An implementation of a central server that shares across the clients the computations done by all.

In English, the computational system will be made of clients running the mechanical computations. One client per celestial body. Each client will then send the update to the central server that will in turn broadcast the new information to every client.

The init phase and subsequent update phases, the central server receives updated data from the clients (left). During the broadcast phase, the central server sends to all clients the data from all clients (right). With the broadcasted data, clients compute the new positions of celestial bodies.

First I will talk about the use of gRPC to generate the Golang files necessary for the implementations, second I will give an overview of the implementation of the server, and third, an overview of the client.

The gRPC protocol

Defined in a .proto file, the protocol states what the transmitted data is like, as well as the services available in the server.

syntax = "proto3";

package taiyoukei;

option go_package = "./proto";

message CelestialBody {
uint64 sequence = 1; // Seqwuence in stream
string name = 2; // Name of the celestial body
double mass = 3; // Mass of the celestial body
double x = 4; // x-position
double y = 5; // y-position
double vx = 6; // x-speed
double vy = 7; // x-speed
}

message CelestialBodiesPositionRequest {}

service CelestialService {
rpc CelestialUpdate(stream CelestialBody) returns (stream Data) {}
rpc CelestialBodiesPositions(CelestialBodiesPositionRequest) returns (stream Data) {}
}

message Data {
bool success = 1;
map<string, CelestialBody> content = 2;
}

There is only one message being sent around: it contains the kinematic information of the body as well as its mass, its name and a sequence number. The sequence number is there to ensure that the flow of messages remains consistent.

As you can see, there are two services defined with the keyword rpc:

  • CelestialUpdate, to be used between the server and the computing clients
  • CelestialBodiesPositions, to be used between the server and an outside watcher used for the analysis of the results.

We will concentrate on the first one as it is the most interesting feature of this server. The second one was straight forward and does not bear any interesting aspects.

To generate the gRPC code I used the command

protoc --go_out=. --go-grpc_out=. --proto_path=proto/ proto/celestial.proto

which produced the files

% tree proto
proto
├── celestial.pb.go # GENERATED
├── celestial.proto # Source file - NOT GENERATED
└── celestial_grpc.pb.go # GENERATED

celestial.pb.go and celestial_grpc.pb.go contain the boilerplate code for the server and clients infrastructures. The specialized logic for the application we are writting is, however, completely absent.

And that is what I present below.

The Server

Here are the requirements for the server

(i) — it waits for all updates from the clients

(ii) — once all update data is received, it broadcasts it to all clients

When the clients receive the broadcast, they have the positions of all other bodies and they can, in turn, update their position using the equations of motion and resend an update to the server, that resends a broadcast once all updates are received again, etc.

The mental image I have of the data flow between the server and each client is similar to this

Diagram of the data flow between the server and each client (one level per client)

The general structure of the CelestialUpdate program is given below.

/* 
SERVICE -- bidirectional streaming with clients: reception of updates
and broadcast of these updates to all clients.
*/

func (s *server) CelestialUpdate(stream pb.CelestialService_CelestialUpdateServer) error {

/* Connection object definition and storage in map */

stop := make(chan bool) // -> Channel to ensure smooth stop

if n_connections == ONE {
go func() {
/*
Handles the emission of broadcasts
- only started when there is at least one connected client
- killed when the number of clients drops to zero
*/
err := s.sendData(&data)
if err != nil {
stop <- true // Stops the loop forloop in the parent routine
break
}
}()
}

log.Printf("Initiating data reception for id %v", id)
forloop:
for {
select {
case <-stop:
break forloop
default:
/*
Handles the reception of client updates
*/
}
}
return nil
}

CelestialUpdate is a method of an interface generated from the .proto file. And in the generic code shown above, I basically implement the interface for a server struct that I defined myself.

The most important feature of this method is the go-routine started under the condition if n_connection == ONE. This go-routine will be in charge with the broadcast of the received data. Without this simple design, I was literally banging my head trying to find an easy way to ensure the synchronicity between client threads and the broadcast…

Having a dedicated go-routine made everything a lot easier, even if I have to admit that, in terms of design, it is not ideal.

Now that I am writing these lines, I wonder if it would not have been better to have both client- and server-streams instead a single bi-directional stream*…

* I’ll let you tell me in the comments what you think would have been a better solution.

The Client

As for the client, there is nothing particularly difficult with regards to the gRPC communications. The interesting part was in the solver that I implemented to simulate the orbits.

Numerical Solver — Runge-Kutta of order 4 (RK4), a go-to solver stable enough to have a coherent system and easy enough to not loose your hair with endless stability algebra.

Using the previously introduced generic equation

the RK4 algorithm states that for a discrete step Δt the sequence of discrete vectors σₙ is given by the following recursion

Now here is the fantastic part: using a strategy design pattern in Go, I was able to very easily implement the RK4. I did not even have to debug the algorithm, from the get-go, it was correct — which is rare! Not that I am particularly amazing at writing all of this, nor that ChatGPT gave me the answer (I made a point of honor to implement things myself), but by staying as close as possible to the mathematical statements, chances of mistake are greatly reduced. So,

(i) — I introduced an implementation of vector σ, hamiltonVector below, with all the algebraic necessary tools (addition and scalar multiplications)

(ii) — I introduced a rateFunction type in the signature of my solver, providing a strategy like design pattern for the rate function

(iii) — I introduced a Solver interface providing another strategy-like design pattern for the solver this time.

/* 
STRATEGY PATTERN 1 TO SOLVE THE ODE ON RATE FUNCTION
*/
type rateFunction func(hamiltonVector) hamiltonVector

/*
STRATEGY PATTERN 2 TO SOLVE THE ODE ON SOLVER
*/
type Solver interface {
step(sigma hamiltonVector, f rateFunction, dt float64) hamiltonVector
}

type RungeKutta4Solver struct {
}

/*
Implementation of RK4
*/
func (rks *RungeKutta4Solver) step(sigma hamiltonVector, f rateFunction, dt float64) hamiltonVector {
F1 := f(sigma)
F2 := f(sigma.Add(F1.scalarMultiply(dt / 2.0)))
F3 := f(sigma.Add(F2.scalarMultiply(dt / 2.0)))
F4 := f(sigma.Add(F3.scalarMultiply(dt)))
sum_F := F1.Add(F2.scalarMultiply(2.0))
sum_F = sum_F.Add(F3.scalarMultiply(2.0))
sum_F = sum_F.Add(F4)
return sigma.Add(sum_F.scalarMultiply(dt / 6.0))
}

type celestialRateFunctionHandler struct {
bodies []hamiltonVector
masses []float64
}

/*
Equations of motion -- specific implementation of the rate function
*/
func (fh *celestialRateFunctionHandler) evaluate(sigmai hamiltonVector) hamiltonVector {
rate := hamiltonVector{
x: sigmai.vx,
y: sigmai.vy,
vx: 0.,
vy: 0.,
}
for j, sigmaj := range fh.bodies {
muj := fh.masses[j]
d_ij := math.Sqrt((sigmaj.x-sigmai.x)*(sigmaj.x-sigmai.x) + (sigmaj.y-sigmai.y)*(sigmaj.y-sigmai.y))
Fx := muj * (sigmaj.x - sigmai.x) / (d_ij * d_ij * d_ij)
Fy := muj * (sigmaj.y - sigmai.y) / (d_ij * d_ij * d_ij)
rate.vx += Fx
rate.vy += Fy
log.Printf("mu = %v d = %v, F = %v", muj, d_ij, math.Sqrt(Fx*Fx+Fy*Fy))
}
return rate
}

Going further — You ought to know that RK4 eventually diverges. State of the art orbital computations are now performed with the Gauss-Jackson algorithm.

But…

  • It is a lot harder and it would have to be an blogpost by itself.
  • RK4 is perfect for a first version of the computing system.
  • With the strategy pattern, it will be a breeze to modify the algorithm used to propagate the orbits in the clients.

Results

Now, after all this hardworking, the results. At first, here are the initial conditions I gave to the the system.

  • I assumed that the Sun, Earth and Moon are all in the same plane. This assumption was there from the beginning. It is actually wrong because there is a 5 degree incline between the Earth Orbital Plane and the Moon orbital plane. But we (secretly) neglected that.
  • There has to be a starting point when all three are aligned — e.g. during an eclipse. And I chose the alignment SME. And this allowed me to set all the y values to 0.
  • Because of the masses, the Sun is about a million times more massive than the Earth which is in turn about a hundred times more massive than the Moon. So it seemed fair that G would be the same as S, leading to the values
  • As for the velocities, the masses were again such that in first approximations

These initial conditions were plain wrong. Way too approximative! I would always get a divergence of the Moon as it would get to close to Earth. Which would happen everytime because of their initial position and speed. Eventually the Moon would even have its own orbit around the Sun. 😇

So I took my pen, stopped being lazy and rederived appropriate initial conditions. Here is what I got (in adimensional form).

  • For the initial positions:
  • For the initial velocities

As the Earth and the Moon both orbit counter-clockwise from a North Pole view point. The Moon’s velocity above is set to the orbital velocity of the Earth minus the orbital velocity of the Moon around the Earth. This is because the initial state is in SME configuration.

Had it been in SEM configuration (the Sun, then the Earth, and finally the Moon), the velocities would have been added and not subtracted!

Back when I was preparing for the Ecole Polytechnique, getting Numerical Values of Theoretical Formulae was always a nightmare. I would always mess up something. But thank god, ChatGPT is here to the rescue.

Here is a screenshot of my request and his response.

Letting chatGPT provide me with the numerical input values

Using these values as input, I enter these commands (in different tabs of my terminal)

# To start the server
go run server/run.go

# To start the capture client -- not detailed in the text but use it
# to generate the orbit graphs
go run capture/main.go

# To start the Sun
go run client/run.go -name S -mass 1 -x -0.0000030393 -y 0 -vx 0 -vy 0

# To start the Earth
go run client/run.go -name E -mass 0.0000030025 -x 0.999997 -y 0 -vx 0 -vy 1

# To start the Moon
go run client/run.go -name M -mass 0.000000036938 -x 0.997427 -y 0 -vx 0 -vy 0.965816

The computations will only start once all the expected clients are connected — here 3 clients are expected.

Now let’s look ate the plots generated by capture/main.go to assess whether or not the simulation makes sense.

Scroll down a little more….

ET VOILÀ!

Note: the Sun barely moves and therefore its orbital trajectory could not be plotted.

This output looked extremly promising as you could see that the Earth E orbits around the Sun S and so does the Moon M. In my first trials the Moon was just wrongly being thrown into a different orbit around the Sun than the Earth’s one.

Also, note how the Moon orbit (in green) seems to be crossing back and forth the Earth Orbit (in red). This is a possible indication that the Moon is, as expected from observations, orbiting around the Earth as well.

So let’s check this by centering the graphs on the Earth.

Scroll down a little more….

(Yes I am that annoying)

Note: The Moon seems to have a deformed orbit. It could be because of Numerical errors or because of the Sun’s gravitational pull. I have not conducted the investigation, but my gut feeling is that it could be from the Sun’s gravitational pull… What do you think?

やったあああああああああああああああああ!

--

--

Gil-Arnaud Coche

What am I? I am not sure... An engineer? A physicist? A mathematician? What I know is that I terribly enjoy to model stuff.