NBodyJS is a simple project I’ve had around for the past few years in various forms. It is a JavaScript n-body simulation. It is currently written in Kotlin. Initially I wrote it in plain JavaScript, then re-wrote in Scala, and finally, re-wrote in Kotlin. Both the Scala and Kotlin versions transpile to readable JavaScript, using ScalaJS for Scala and Kotlin’s built-in JavaScript support. The Kotlin implementation is by far my favorite, and the language itself is a joy to write in. NBodyJS utilizes the HTML5 canvas element for display, and kotlin-js to transpile Kotlin to JavaScript.

See it in action and play around! You can also view the code on GitHub.

Some Background on N-body Simulations (warning: math!)

Within a gravity simulation composed of \(n\) bodies, the magnitude of the force \(\mathbf{F}_i\) acting on each body \(i\), is determined by Newton’s law of universal gravitation:

\[\mathbf{F}_i = \sum_{j = 1\atop j\not = i}^n G\frac{m_i m_j}{|\mathbf{r}_{ij}^2|}\hat{\mathbf{r}}_{ij}\]

where \(G\) is the gravitational constant, \(m_i\) is the mass of body \(i\), \(m_j\) is the mass of body \(j\), and \(\mathbf{r}_{ij}\) is the distance vector from \(i\) to \(j\).

In the case of gravity simulations, the forces due to gravity get exceedingly large as bodies get closer to each other (due to the inverse-square law nature of the gravitational force), and hence bodies tend to rapidly accelerate and zoom off when they seemingly “collide”. To compensate for this, a softening length \(R^2\) has been applied in the gravity calculation to prevent bodies from zipping off into the abyss when they get near each-other. Also, \(G\) has been set to 1 for simplicity in NBodyJS.

We can then use Newton’s second law \(\mathbf{F} = m\mathbf{a}\) to solve for the acceleration, and then numerically integrate it to get the positions and velocities of all bodies. In Kotlin, the code to compute the acceleration due to gravity looks like this:

fun gravityAcceleration(x: Vector, bodies: Set<Body>): Vector {
  fun gravity(pos: Vector, body2: Body): Vector {
    val r12 = body2.x - pos
    return r12 * (body2.m * G) / Math.pow(Math.pow(r12.len, 2.0) + Math.pow(SOFTENING_LENGTH, 2.0), 3/2.0)
  }
  return bodies.map { body -> gravity(x, body) }.fold(Vector.zero) { a1, a2 -> a1 + a2 }
}

The Algorithm

To compute the positions and velocities of all of the bodies, NBodyJS utilizes the Velocity Verlet method of numerical integration. Verlet integration is a second-order method, which offers more stability than simpler, first order solutions such as Euler’s method, yet at no significant additional computational cost.

For the algorithm to work, first you need to specify the initial conditions of the simulation; namely the initial positions and velocities of all the bodies at some start time \(t\). Then, the Velocity Verlet algorithm consists of the following steps:

  1. Calculate the acceleration \(\mathbf{a}(t)\) of each body due to gravity (or whatever force(s) you want) at time \(t\), using Newton’s laws.
  2. Calculate the position \(\mathbf{x}(t + dt)\) of each body at time \(t + dt\) (where \(dt\) is a small time step), using the approximation

    \[\mathbf{x}(t + dt) = \mathbf{x}(t) + \mathbf{v}(t)dt + \mathbf{a}(t)\frac{dt^2}{2}\]
  3. Calculate the acceleration \(\mathbf{a}(t + dt)\), again using Newton’s laws and the forces in play.
  4. Calculate the velocity \(\mathbf{v}(t + dt)\) using the approximation

    \[\mathbf{v}(t + dt) = \mathbf{v}(t) + (\mathbf{a}(t) + \mathbf{a}(t + dt))\frac{dt}{2}\]

In Kotlin, the integration code is extremely simple:

fun verlet(x: Vector, v: Vector, dt: Double, a: (Vector) -> Vector): Pair<Vector, Vector> {
  val x1 = (x + (v * dt)) + (a(x) * (Math.pow(dt, 2.0) / 2.0))
  val v1 = v + ((a(x) + a(x1)) * (dt / 2))
  return Pair(x1, v1)
}

This algorithm is executed every time you take a time step, \(dt\). Since every body exerts a force on every other body, computing the accelerations is done in \(O(n^2)\) time.

There are more efficient, yet similarly accurate algorithms out there, so feel free to implement your own version if you feel like experimenting! Also, note that this method can be used to integrate Newton’s laws for nearly any force(s), not just gravity!