Contents

Advent of Code 2025 - Day 7: Laboratories

Day 7 was about tachyon beams splitting through a manifold. Part 1 was a straightforward recursive beam tracer. Part 2 took a completely different approach with a quantum twist.

Part 1: Classical Beam Splitting

A tachyon beam enters at S and travels downward. When it hits a splitter (^), it stops and creates two new beams going down-left and down-right.

Example:

1
2
3
4
5
.......S.......
.......|.......
......|^|......
......|.|......
.....|^|^|.....

Count how many times the beam splits.

The Recursive Solution

This is perfect for recursion: a beam travels down until it hits something, then potentially spawns more beams.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
def shoot_rays(grid: list, pos: set, rays: list, score: list):
    rays.append(pos)
    down = get_value_in_direction(grid, pos, 'down')
    
    # Exit condition: already visited this position
    if down in rays:
        return rays, score
    
    # Empty space: keep moving down
    if down == '.':
        pos = addTuples(pos, (1, 0))
        return shoot_rays(grid, pos, rays, score) 
    
    # Splitter: split into two beams
    elif down == '^':
        if addTuples(pos, (1, 0)) not in score:
            score.append(addTuples(pos, (1, 0)))
        
        left = addTuples(pos, (1, -1))
        right = addTuples(pos, (1, 1))
        
        if not grid_valid(left[0], left[1], grid):
            return rays, score
        
        if left not in rays:
            rays.append(left)
            r, s = shoot_rays(grid, left, rays, score)
        
        if right not in rays:
            rays.append(right)
            return shoot_rays(grid, right, rays, score)
    
    return rays, score

The logic:

  1. Track visited positions in rays to avoid infinite loops
  2. If we see ., move down and recurse
  3. If we see ^, record the split and spawn two recursive calls (left and right)

Debugging with Visualization

My nprint() helper made debugging much easier. It prints the grid with colored markers showing the beam paths:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
   . . . . . . . S . . . . . . . 
   . . . . . . . | . . . . . . . 
   . . . . . . | ^ | . . . . . . 
   . . . . . . | . | . . . . . . 
   . . . . . | ^ | ^ | . . . . . 
   . . . . . | . | . | . . . . . 
   . . . . | ^ | ^ | ^ | . . . . 
   . . . . | . | . | . | . . . . 
   . . . | ^ | ^ | | | ^ | . . . 
   . . . | . | . | | | . | . . . 
   . . | ^ | ^ | | | ^ | ^ | . . 
   . . | . | . | | | . | . | . . 
   . | ^ | | | ^ | | . | | ^ | . 
   . | . | | | . | | . | | . | . 
   | ^ | ^ | ^ | ^ | ^ | | | ^ | 
   | . | . | . | . | . | | | . |

This immediately shows if beams are going the wrong direction or missing splits.

The Challenge: SSH from a Phone

The hardest part? Doing this while Christmas crafting at my parents’ place, SSHing from my phone to my server. Not ideal for debugging recursion, but it worked.

First try after fixing some minor score-counting bugs.

Part 2: Quantum Beam Splitting

Part 2 reveals this is actually a quantum tachyon manifold. A single particle takes both paths at every splitter, creating parallel timelines. Count how many timelines exist after the particle completes all paths.

The Insight

This isn’t about tracing individual paths anymore - it’s about counting how many ways the beam can split at each level.

I checked some visualizations on Reddit because I couldn’t wrap my head around the quantum interpretation. The insight: instead of recursively following beams, track how many beams are at each column as you go down row by row.

The Simple Solution

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
def part2():
    grid = toGrid(input_f)
    start = findInGrid(grid, 'S')
    
    # Initialize: one beam at start column
    count = [0] * len(grid[0])
    count[start[1]] = 1
    
    # Process each row
    for x, cols in enumerate(grid):
        for y, rows in enumerate(cols):
            if grid[x][y] == '^':
                # Split: add current count to left and right columns
                count[y+1] += count[y]
                count[y-1] += count[y]
                count[y] = 0  # This column is now blocked by splitter
        print(count)
    
    return sum(count)

Start with one beam:

1
[0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0]

After hitting the first splitter on row 3:

1
[0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0]

After multiple splits by row 7:

1
[0, 0, 0, 0, 1, 0, 3, 0, 3, 0, 1, 0, 0, 0, 0]

The number at each position represents how many timelines have a beam at that column. When a beam hits a splitter, those timelines split - the count at that column gets added to the adjacent columns.

At the end, sum all the counts to get the total number of timelines.

First try: but with help from Reddit visualizations and pen-and-paper work.

Part 1 vs Part 2

These required completely different approaches:

Part 1 (Classical): Trace individual beam paths recursively. Each beam is independent. Track visited positions to avoid loops.

Part 2 (Quantum): Count timelines at each column level. Process row by row. No path tracing needed - just count propagation.

The quantum interpretation made the problem simpler algorithmically. Instead of exponential path explosion (which would be intractable), we just track counts at each column, which is linear in grid size.

The Lesson

Sometimes Part 2 isn’t “Part 1 but harder” - it’s a completely different problem that requires rethinking your approach. The recursive beam tracer from Part 1 would have been useless for Part 2.

Also: pen and paper helps. Visualizing the count propagation on paper made the algorithm obvious. And checking Reddit for visualization ideas when stuck isn’t cheating - it’s learning from the community.

Fourteen stars total (catching up on Day 7 Part 2 after doing Day 8). Grid problems remain my favorite, especially when they come with quantum twists.