Solving this disc puzzle

One of my friends got this puzzle as a Christmas present. It has five irregular revolving discs with numbers printed on them. The goal is to turn them so that the numbers visible on each column (each radial spoke) adds up to 42.

Five laser-cut wooden discs, irregularly shaped, with numbers on them.
The accursed discs

There are 12^4 = 20,736 possible arrangements of these discs. (Assume the largest disc remains static, and the other four discs can each turn to twelve different positions.) At a brisk pace of five seconds per arrangement, it would take just shy of 29 hours to check all of them.

Can we improve on this? Yes! There are several clever strategies we can use to streamline the brute-force method of examining every arrangement. Sadly I did not think of any of them, aside from the most useful one in my line of work — using a computer. A computer takes far less than five seconds to sum a handful of numbers, and any inefficiencies in our execution are made up for (in this case) by speed.

Here is some code to do that:

from itertools import product
import numpy as np

raw_data = """2 5 10 7 16 8 7 8 8 3 4 12 
3 3 14 14 21 21 9 9 4 4 6 6
8 9 10 11 12 13 14 15 4 5 6 7
14 11 14 14 11 14 11 14 11 11 14 11

1 0 9 0 12 0 6 0 10 0 10 0
3 26 6 0 2 13 9 0 17 19 3 12
9 20 12 3 6 0 14 12 3 8 9 0
7 0 9 0 7 14 11 0 8 0 16 2

0 0 0 0 0 0 0 0 0 0 0 0
5 0 10 0 8 0 22 0 16 0 9 0
21 6 15 4 9 18 11 26 14 1 12 0
9 13 9 7 13 21 17 4 5 0 7 8

0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0
4 0 7 15 0 0 14 0 9 0 12 0
7 3 0 6 0 11 11 6 11 0 6 17

0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0
0 0 0 0 0 0 0 0 0 0 0 0
3 0 6 0 10 0 7 0 15 0 8 0

"""
target_sum = 42

# Build a 3D numpy array from raw data.
def build_discs(raw_data):
    discs = []
    disc = []
    for line in raw_data.splitlines():
        if len(line.strip()) > 0:
            disc.append([int(i) for i in line.strip().split(" ")])
        else:
            discs.append(disc)
            disc = []
    return np.array(discs[::-1])  # flip discs to visible order.


# Collapse 3D numpy array into 2D numpy array.
def collapse_discs(discs):
    return np.apply_along_axis(lambda e: e[(e != 0).argmax()], 0, discs)


# Offset each layer of disc by a given amount.
# Offset is given as an array of offsets for each height.
def offset_discs(discs, offsets):
    discs_copy = np.copy(discs)
    for i, offset in enumerate(offsets):
        discs_copy[i] = [np.roll(line, offset) for line in discs[i]]
    return discs_copy


# Check if array matches target sum.
def check_discs(collapsed_discs):
    sums = np.sum(collapsed_discs, axis=0)
    return np.all(sums == target_sum)


discs = build_discs(raw_data)
for offsets in product(range(12), repeat=4):
    offsets = [0] + list(offsets)
    offsetted_discs = offset_discs(discs, offsets)
    collapsed_discs = collapse_discs(offsetted_discs)
    if check_discs(collapsed_discs):
        print(offsetted_discs)

It represents the discs as a three-dimensional numpy array, simulates rotating them by offsetting their elements, and checks if a particular arrangement is a solution by flattening and then summing the array. It is not very elegant, but it does get the right answer. And now I can finally stop fiddling with this disc.