n-body/src/main.nika

// The Benchmarks Game's `n-body`: the sun and four planets, moved forward in
// small time steps, with the system's total energy printed before and after.
//
//     nikaia run -- 1000
//     -0.169075164
//     -0.169087605
//
// The argument is the number of steps (1000 if left out). These two numbers
// are the benchmark's published output for 1000 steps.
//
// Shows: structs with methods, `ref mut self` for methods that change their
// value, `sync` functions, and floating-point arithmetic in a loop.
//
// A package: `nikaia run` in this directory runs it, and `nikaia test` checks
// what it prints against `tests/` (ADR-269 D13).

use std::cli
use std::text

// One body: a position, a velocity, and a mass. All of it is changed in place,
// since the step below runs millions of times.
pub struct Body {
    x: f64,
    y: f64,
    z: f64,
    vx: f64,
    vy: f64,
    vz: f64,
    mass: f64,
}

// The bodies as a set, because the interesting operations - the pairwise
// force, the total energy - are about all of them at once.
pub struct System {
    bodies: Vec[Body],
}

impl System {

    // The sun's velocity is not given: it is whatever makes the total momentum
    // of the system zero, which keeps the centre of mass still.
    //
    // `sync` says the method cannot pause (no file or network I/O, for
    // example); the compiler refuses the program if it could.
    fn offset_momentum(ref mut self) sync {
        let mut px = 0.0
        let mut py = 0.0
        let mut pz = 0.0

        for i in 0..<self.bodies.len() {
            px = px + self.bodies[i].vx * self.bodies[i].mass
            py = py + self.bodies[i].vy * self.bodies[i].mass
            pz = pz + self.bodies[i].vz * self.bodies[i].mass
        }

        let solar_mass = 4.0 * 3.141592653589793 * 3.141592653589793
        self.bodies[0].vx = -px / solar_mass
        self.bodies[0].vy = -py / solar_mass
        self.bodies[0].vz = -pz / solar_mass
    }

    // One step: first every pair pulls on both of its bodies, then every body
    // moves. Updating all velocities before any position is what keeps the
    // energy from drifting over millions of steps, so the two loops cannot be
    // merged into one.
    //
    // The inner loop starts at `i + 1`: each pair is visited once, and the pull
    // on its two bodies differs only in sign.
    fn advance(ref mut self, dt: f64) sync {
        let n = self.bodies.len()

        for i in 0..<n {
            for j in i + 1..<n {
                let dx = self.bodies[i].x - self.bodies[j].x
                let dy = self.bodies[i].y - self.bodies[j].y
                let dz = self.bodies[i].z - self.bodies[j].z

                let d2 = dx * dx + dy * dy + dz * dz
                let mag = dt / (d2 * d2.sqrt())

                let mi = self.bodies[i].mass
                let mj = self.bodies[j].mass

                self.bodies[i].vx = self.bodies[i].vx - dx * mj * mag
                self.bodies[i].vy = self.bodies[i].vy - dy * mj * mag
                self.bodies[i].vz = self.bodies[i].vz - dz * mj * mag

                self.bodies[j].vx = self.bodies[j].vx + dx * mi * mag
                self.bodies[j].vy = self.bodies[j].vy + dy * mi * mag
                self.bodies[j].vz = self.bodies[j].vz + dz * mi * mag
            }
        }

        for i in 0..<n {
            self.bodies[i].x = self.bodies[i].x + dt * self.bodies[i].vx
            self.bodies[i].y = self.bodies[i].y + dt * self.bodies[i].vy
            self.bodies[i].z = self.bodies[i].z + dt * self.bodies[i].vz
        }
    }

    // Kinetic energy of each body, less the potential energy of each pair. An
    // exact integrator would keep it constant, so printing it before and after
    // shows how well the arithmetic held up.
    fn energy(ref self) -> f64 sync {
        let n = self.bodies.len()
        let mut e = 0.0

        for i in 0..<n {
            let b = self.bodies[i]
            e = e + 0.5 * b.mass * (b.vx * b.vx + b.vy * b.vy + b.vz * b.vz)

            for j in i + 1..<n {
                let dx = b.x - self.bodies[j].x
                let dy = b.y - self.bodies[j].y
                let dz = b.z - self.bodies[j].z
                e = e - (b.mass * self.bodies[j].mass) / (dx * dx + dy * dy + dz * dz).sqrt()
            }
        }

        return e
    }
}

// The sun, Jupiter, Saturn, Uranus and Neptune, in the benchmark's units:
// astronomical units, years, and a solar mass of 4 pi squared. The velocities
// are given per day, so each is multiplied by the days in a year.
fn solar_system() -> System {
    let days_per_year = 365.24
    let solar_mass = 4.0 * 3.141592653589793 * 3.141592653589793

    let bodies = [
        Body {
            x: 0.0, y: 0.0, z: 0.0,
            vx: 0.0, vy: 0.0, vz: 0.0,
            mass: solar_mass,
        },
        Body {
            x: 4.84143144246472090e+00,
            y: -1.16032004402742839e+00,
            z: -1.03622044471123109e-01,
            vx: 1.66007664274403694e-03 * days_per_year,
            vy: 7.69901118419740425e-03 * days_per_year,
            vz: -6.90460016972063023e-05 * days_per_year,
            mass: 9.54791938424326609e-04 * solar_mass,
        },
        Body {
            x: 8.34336671824457987e+00,
            y: 4.12479856412430479e+00,
            z: -4.03523417114321381e-01,
            vx: -2.76742510726862411e-03 * days_per_year,
            vy: 4.99852801234917238e-03 * days_per_year,
            vz: 2.30417297573763929e-05 * days_per_year,
            mass: 2.85885980666130812e-04 * solar_mass,
        },
        Body {
            x: 1.28943695621391310e+01,
            y: -1.51111514016986312e+01,
            z: -2.23307578892655734e-01,
            vx: 2.96460137564761618e-03 * days_per_year,
            vy: 2.37847173959480950e-03 * days_per_year,
            vz: -2.96589568540237556e-05 * days_per_year,
            mass: 4.36624404335156298e-05 * solar_mass,
        },
        Body {
            x: 1.53796971148509165e+01,
            y: -2.59193146099879641e+01,
            z: 1.79258772950371181e-01,
            vx: 2.68067772490389322e-03 * days_per_year,
            vy: 1.62824170038242295e-03 * days_per_year,
            vz: -9.51592254519715870e-05 * days_per_year,
            mass: 5.15138902046611451e-05 * solar_mass,
        },
    ]

    return System { bodies: bodies }
}

fn main() {
    // `??` is for a value that may be absent: `nth` returns one. Parsing is
    // different, it can fail, so its fallback is a `catch` whose block gives
    // the value to use instead.
    let steps = cli::args().nth(1) ?? "1000"
    let n = text::parse_i32(steps) ?? 1000

    let mut system = solar_system()
    system.offset_momentum()

    println(f"{system.energy():.9}")
    for _step in 0..<n {
        system.advance(0.01)
    }
    println(f"{system.energy():.9}")
}

The file itself · all the examples

This site is open source. Improve this page.