Sean McLemon | Advent of Code

Home | Czech | Blog | GitHub | Advent Of Code | Notes


2019-12-12 - The N-Body Problem

(original .ipynb)

Day 12 puzzle input is the position and velocity of a number of moons (mine is here but I didn't want to write any parsing code and it's only 4 lines so I have inlined it in my solution below). Part 1 involves calculating the movement of these moons over 1000 "steps" and applying a calculation to return the "energy" in the system. Part 2 involves attempting to find how many steps it would take for all of the moons to return to their original positions.

from itertools import combinations

axis_x = 0
axis_y = 1
axis_z = 2

def signed_unit(val):
    if val == 0:
        return 0
    return int(abs(val)/val)

def velocity_on_axis(moon0_pos, moon1_pos, axis):
    difference = signed_unit(moon0_pos[axis] - moon1_pos[axis])
    return (-difference, difference)    

def sum_velocity(vel1, vel2):
    return [
        vel1[axis_x] + vel2[axis_x],
        vel1[axis_y] + vel2[axis_y],
        vel1[axis_z] + vel2[axis_z]
    ]

def apply_velocity(moon_pos, vel):
    return sum_velocity(moon_pos, vel)

# To apply gravity, consider every pair of moons. On each axis (x, y, and z), the
# velocity of each moon changes by exactly +1 or -1 to pull the moons together. For
# example, if Ganymede has an x position of 3, and Callisto has a x position of 5, 
# then Ganymede's x velocity changes by +1 (because 5 > 3) and Callisto's x velocity changes 
# by -1 (because 3 < 5).

assert (1, -1) == velocity_on_axis([3], [5], 0)
assert (-1, 1) == velocity_on_axis([10], [5], 0)
assert (0, 0) == velocity_on_axis([5], [5], 0)

def calculate_step(moon_positions, moon_velocity):
    
    moon_indices = range(0, len(moon_positions))
    moon_pairs = combinations(moon_indices, 2)
       
    for (moon0, moon1) in moon_pairs:
        moon0_pos = moon_positions[moon0]
        moon1_pos = moon_positions[moon1]
        
        (moon0_vel_x, moon1_vel_x) = velocity_on_axis(moon0_pos, moon1_pos, axis_x)
        (moon0_vel_y, moon1_vel_y) = velocity_on_axis(moon0_pos, moon1_pos, axis_y)
        (moon0_vel_z, moon1_vel_z) = velocity_on_axis(moon0_pos, moon1_pos, axis_z)
        
        moon_velocity[moon0] = sum_velocity(moon_velocity[moon0], [ moon0_vel_x, moon0_vel_y, moon0_vel_z ])
        moon_velocity[moon1] = sum_velocity(moon_velocity[moon1], [ moon1_vel_x, moon1_vel_y, moon1_vel_z ])
        
    return (
        [ apply_velocity(moon_pos, moon_velocity[moon]) for moon, moon_pos in enumerate(moon_positions) ],
        moon_velocity
    )

def calc_energy(values):
    return sum( [ abs(v) for v in values ] )

def calc_total_energy(positions, velocities):
    # A moon's potential energy is the sum of the absolute values of its x, y, and z position coordinates.
    potential_energies = [ calc_energy(position) for position in positions ]

    # A moon's kinetic energy is the sum of the absolute values of its velocity coordinates
    kinetic_energies = [ calc_energy(velocity) for velocity in velocities ]
    
    energies = zip(potential_energies, kinetic_energies)

    # The total energy for a single moon is its potential energy multiplied by its kinetic energy.
    return sum([ potential * kinetic for (potential, kinetic) in energies ])

test_positions_0 = [
    [-1, 0, 2],
    [2, -10, -7],
    [4, -8, 8],
    [3, 5, -1]
]

test_positions_1 = [
    [ -8, -10, 0 ],
    [ 5, 5, 10 ],
    [ 2, -7, 3 ],
    [ 9, -8, -3 ]
]



def all_cycles_found(cycle_times):
    return cycle_times[axis_x] > 0 and cycle_times[axis_y] > 0 and cycle_times[axis_z] > 0

def perform_simulation(num_steps, positions):

    original_positions = [ (x, y, z) for (x,y,z) in positions ]
                                                          
    current_step = 0
    velocities = [ [0, 0, 0] for n in positions ]
        
    while current_step < num_steps:
        positions, velocities = calculate_step(positions, velocities)
        current_step += 1
        
    return positions, velocities


def energy_after_steps(num_steps, positions):
    positions, velocities = perform_simulation(num_steps, positions)
    return calc_total_energy(positions, velocities)

assert 179 == energy_after_steps(10, test_positions_0)
assert 1940 == energy_after_steps(100, test_positions_1)
puzzle_positions = [
    [1, 3, -11],
    [17, -10, -8], 
    [-1, -15, 2],
    [12, -4, -4]
]

print(energy_after_steps(1000, puzzle_positions))
8310
import math

# NOTE: I had to search around for help, it clicked once someone mentioned the least-common-multiple
#       so consider this slightly tainted :-(

def lcm(a, b):
    return a * b // math.gcd(a, b)

def find_cycle_times(positions):
    original_positions = [ [x, y, z] for x,y,z in positions ]
    current_step = 0
    cycle_times = [ 0, 0, 0 ]
    velocities = [ [0, 0, 0] for n in positions ]
        
    while not(all_cycles_found(cycle_times)):
        positions, velocities = calculate_step(positions, velocities)
        current_step += 1
        for axis in [axis_x, axis_y, axis_z]:
            if cycle_times[axis] == 0:
                back_to_original = True
                
                for i, position in enumerate(positions):
                    back_to_original = back_to_original and position[axis] == original_positions[i][axis] and velocities[i][axis] == 0
                    
                if back_to_original:
                    cycle_times[axis] = current_step
                
    return lcm(lcm(cycle_times[axis_x], cycle_times[axis_y]), cycle_times[axis_z])

assert 2772 == find_cycle_times(test_positions_0)
assert 4686774924 == find_cycle_times(test_positions_1)

print(find_cycle_times(puzzle_positions))
319290382980408