Sean McLemon | Advent of Code

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


2021-12-19 - Beacon Scanner

(original .ipynb)

Day 19 puzzle input is a collection of scanners and the relative 3D positions of scanners they can see (mine is here). Part 1 involves translating and rotating these to find how the scanners all fit together and finding the number of beacons overall. Part 2 involves finding the maximum manhattan distance between any two scanners.

A warning to anyone who wants to look at or enjoy the code contained within:

This place is not a place of honor... no highly esteemed deed is commemorated here... nothing valued is here. What is here was dangerous and repulsive to us. This message is a warning about danger.

tiny_test_str = """--- scanner 0 ---
0,2,0
4,1,0
3,3,0

--- scanner 1 ---
-1,-1,0
-5,0,0
-2,1,0"""

test_input_str = """--- scanner 0 ---
404,-588,-901
528,-643,409
-838,591,734
390,-675,-793
-537,-823,-458
-485,-357,347
-345,-311,381
-661,-816,-575
-876,649,763
-618,-824,-621
553,345,-567
474,580,667
-447,-329,318
-584,868,-557
544,-627,-890
564,392,-477
455,729,728
-892,524,684
-689,845,-530
423,-701,434
7,-33,-71
630,319,-379
443,580,662
-789,900,-551
459,-707,401

--- scanner 1 ---
686,422,578
605,423,415
515,917,-361
-336,658,858
95,138,22
-476,619,847
-340,-569,-846
567,-361,727
-460,603,-452
669,-402,600
729,430,532
-500,-761,534
-322,571,750
-466,-666,-811
-429,-592,574
-355,545,-477
703,-491,-529
-328,-685,520
413,935,-424
-391,539,-444
586,-435,557
-364,-763,-893
807,-499,-711
755,-354,-619
553,889,-390

--- scanner 2 ---
649,640,665
682,-795,504
-784,533,-524
-644,584,-595
-588,-843,648
-30,6,44
-674,560,763
500,723,-460
609,671,-379
-555,-800,653
-675,-892,-343
697,-426,-610
578,704,681
493,664,-388
-671,-858,530
-667,343,800
571,-461,-707
-138,-166,112
-889,563,-600
646,-828,498
640,759,510
-630,509,768
-681,-892,-333
673,-379,-804
-742,-814,-386
577,-820,562

--- scanner 3 ---
-589,542,597
605,-692,669
-500,565,-823
-660,373,557
-458,-679,-417
-488,449,543
-626,468,-788
338,-750,-386
528,-832,-391
562,-778,733
-938,-730,414
543,643,-506
-524,371,-870
407,773,750
-104,29,83
378,-903,-323
-778,-728,485
426,699,580
-438,-605,-362
-469,-447,-387
509,732,623
647,635,-688
-868,-804,481
614,-800,639
595,780,-596

--- scanner 4 ---
727,592,562
-293,-554,779
441,611,-461
-714,465,-776
-743,427,-804
-660,-479,-426
832,-632,460
927,-485,-438
408,393,-506
466,436,-512
110,16,151
-258,-428,682
-393,719,612
-211,-452,876
808,-476,-593
-575,615,604
-485,667,467
-680,325,-822
-627,-443,-432
872,-547,-609
833,512,582
807,604,487
839,-516,451
891,-625,532
-652,-548,-490
30,-46,-14"""

puzzle_input_str = open("puzzle_input/day19.txt").read()

def scan_beacon(line):
    return tuple(int(s) for s in line.split(","))

def parse_scanner(scanner_str):
    lines = scanner_str.split("\n")
    identifier = lines[0].removeprefix("--- scanner ").removesuffix(" ---")
    beacons = [scan_beacon(line) for line in lines[1:]]
    return int(identifier), beacons


def parse_input(input_str):
    scanners = list(parse_scanner(scanner) for scanner in input_str.split("\n\n"))
    return {i:beacons for i,beacons in scanners}


def rot_x(x, y, z):
    return (x, -z, y)

def rot_y(x, y, z):
    return (z, y, -x)

def rot_z(x, y, z):
    return (y, -x, z)

def flip(x, y, z):
    return (-x, -y, -z)

def rotations(point, r):
    for _ in range(4):
        yield point
        point = r(*point)
        

from itertools import permutations, product
from collections import defaultdict

def orientations2(point):
    for f in [point, flip(*point)]:
        for rx in rotations(f, rot_x):
            for ry in rotations(rx, rot_y):
                for p in rotations(ry, rot_z):
                    yield p
                    
def orientations(point):          
    for perm in permutations(range(3)):
        for muls in product([-1, 1], [-1, 1], [-1, 1]):                    
            yield (
                point[perm[0]] * muls[0],
                point[perm[1]] * muls[1],
                point[perm[2]] * muls[2]
            )
            
def orient_funcs():
    for perm in permutations(range(3)):
        for muls in product([-1, 1], [-1, 1], [-1, 1]):                    
            yield lambda point:(
                point[perm[0]] * muls[0],
                point[perm[1]] * muls[1],
                point[perm[2]] * muls[2]
            )

def vec_add(ax, ay, az, bx, by, bz):
    return (ax+bx, ay+by, az+bz)            
            
def vec_sub(ax, ay, az, bx, by, bz):
    return (ax-bx, ay-by, az-bz)
            
def test_orientation(known_beacons, beacons, orient, required_beacons_aligned):
    differences = defaultdict(lambda:0)

    assert len(known_beacons) == len(beacons)
    
    for (known_beacon, beacon) in zip(known_beacons, beacons):
        diff = vec_sub(*known_beacon, *orient(beacon))
        differences[diff] += 1
        
    for diff, count in differences.items():
        if count >= required_beacons_aligned:
            return diff
        
    return None
    
            
def try_orient_scanner_old(all_beacons, scanner_positions, known_scanner, scanner, required_beacons_aligned):
    known_scanner_pos = scanner_positions[known_scanner]
    known_beacons = all_beacons[known_scanner]
    
    for beacons in permutations(all_beacons[scanner]):
        for orient in orient_funcs():
            scanner_position = test_orientation(known_beacons, beacons, orient, required_beacons_aligned)
            if scanner_position is not None:
                scanner_positions[scanner] = scanner_position
                all_beacons[scanner] = [vec_add(*scanner_position, *orient(beacon)) for beacon in beacons]
                return True
    
    return False
            

# areSetsAligned (set1: point[], set2: point[] )
#   for i=0 to set1.length-1
#      for j=0 to set2.length-1
#          let offset = set2[j] - set1[i]  // these 2 points will be aligned
#           let matches = 0;
#           // or we can have 2 loops
#           allPoints1 = Set of set1  //hashtable or whatever
#           // for each point from set2, move it and check if it exists in set1
#           for k=0 to set2.length-1
#             let moved = set2[k] - offset
#             if allPoints.has moved then
#                 matches += 1
#           if matches>=12 then yahoo & break // sets1 and sets2 match, use offset    

def are_sets_aligned(set1, set2, required_beacons_aligned):
    for i in set1:
        for j in set2:
            offset = vec_sub(*j, *i)
            matches = 0
            for k in set2:
                moved = vec_sub(*k, *offset)
                
                if moved in set1:
                    matches += 1
            if matches >= required_beacons_aligned:
                return offset
    return None
    

def try_orient_scanner(all_beacons, scanner_positions, known_scanner, scanner, required_beacons_aligned):
    known_scanner_pos = scanner_positions[known_scanner]
    known_beacons = all_beacons[known_scanner]
    
    for orient in orient_funcs():
        unknown = [tuple(orient(p)) for p in all_beacons[scanner]]
        if offset := are_sets_aligned(known_beacons, unknown, required_beacons_aligned):
            all_beacons[scanner] = [vec_sub(*beacon, *offset) for beacon in unknown]
            scanner_positions[scanner] = offset
    
#     for beacons in permutations(all_beacons[scanner]):
#             scanner_position = test_orientation(known_beacons, beacons, orient, required_beacons_aligned)
#             if scanner_position is not None:
#                 scanner_positions[scanner] = scanner_position
#                 all_beacons[scanner] = [vec_add(*scanner_position, *orient(beacon)) for beacon in beacons]
            return True
    
    return False
    
        
def part_one(input_str, required_beacons_aligned=12):
    all_beacons = parse_input(input_str)
    scanner_positions = {0:(0,0,0)}
    done_scanners = [0]
    todo_scanners = [i for i in all_beacons if i != 0]
    
    while len(todo_scanners) > 0:
        ts = todo_scanners.pop(0)
        for ds in done_scanners:
            success = try_orient_scanner(all_beacons, scanner_positions, ds, ts, required_beacons_aligned)
            if success:
                done_scanners.append(ts)
                break
                
        if ts not in done_scanners:
            todo_scanners.append(ts)
    
    # naive dumb way to do this
    unique_beacons = set()
    for i, beacons in all_beacons.items():
        for beacon in beacons:
            unique_beacons.add(serialize(*beacon))

    from pprint import pprint
    pprint(scanner_positions)
    
    return len(unique_beacons)


def serialize(x,y,z):
    return f"{x},{y},{z}"


assert 3 == part_one(tiny_test_str, 3)
assert 79 == part_one(test_input_str)
print("part one:", part_one(puzzle_input_str))
ok
part one: 362
from itertools import combinations

# i'm not rewriting this today - after the above took forever in my jupyter
# notebook I realised I didn't save the scanner positions, so ran it (slightly)
# quicker on my local machine and dumped the positions using `pprint()`
# whether I come back to this to fix remains to be seen ...

positions = {0: (0, 0, 0),
 1: (2464, 3573, 1227),
 2: (3722, 6019, 87),
 3: (2540, 2433, 1155),
 4: (1219, 3654, -1060),
 5: (-22, 3572, 1159),
 6: (1224, 4801, 1291),
 7: (2544, 4815, 1258),
 8: (94, 2376, 1345),
 9: (4867, 3702, 2531),
 10: (1333, 3618, 24),
 11: (2434, 7343, -40),
 12: (106, 3706, 2449),
 13: (3620, 6069, 1317),
 14: (2506, 3612, 2528),
 15: (1221, 3606, 1168),
 16: (2543, 4769, 88),
 17: (3607, 3589, 2467),
 18: (1233, 3560, 3625),
 19: (3728, 7224, 1252),
 20: (2476, 3556, 4761),
 21: (2437, 7347, 1231),
 22: (63, 1315, 111),
 23: (2521, 3593, 3700),
 24: (143, 2477, 86),
 25: (2350, 5964, 1282),
 26: (26, 1217, -1093),
 27: (2418, 3746, -37)}

def part_two():
    return max(manhattan_distance(*pair) for pair in combinations(positions.values(), 2))
    
print("part two:", part_two())
part two: 12204