from collections import Counter, defaultdict import numpy as np def fit_homography(P1, P2): # p is a size (N, 3) set of 3d points # X is a size (N, 4) set of 3d points N, _ = P1.shape M = np.zeros((N*3, 12)) b = np.zeros((N*3, 1)) for i, (p, y) in enumerate(zip(P1, P2)): b[i*3:i*3+3, 0] = y M[i*3, :3] = p M[i*3 + 1, 3:6] = p M[i*3 + 2, 6:9] = p M[i*3: i*3 + 3, 9:] = np.eye(3) x = np.linalg.inv(M.T @ M) @ M.T @ b R = x[:9].reshape(3,3).round().astype(int) t = x[9:].round().astype(int) return R, t class SensorData: def __init__(self, points: np.ndarray): self.points = points self.point_to_dists = defaultdict(set) self.sensors = np.zeros((1,3)) # Get dists for points self._find_dists() def _find_dists(self): self.point_to_dists = defaultdict(set) for p1 in self.points: for p2 in self.points: if tuple(p1) == tuple(p2): continue d = ((p1 - p2) ** 2).sum().item() self.point_to_dists[tuple(p1)].add(d) self.point_to_dists[tuple(p2)].add(d) def add_points(self, other): matches = self.match_points(other) if matches is None or matches.shape[0] < 12: return False P1, P2 = self.points[matches[:,0], :], other.points[matches[:,1], :] R, t = fit_homography(P2, P1) transformed = other.points @ R.T + t.T self.points = np.concatenate([self.points, transformed]) self.points = np.unique(self.points, axis=0) # self._find_dists() for new_point, old_point in zip(transformed, other.points): self.point_to_dists[tuple(new_point)].update(other.point_to_dists[tuple(old_point)]) # Also map the sensor location to store list of sensors self.sensors = np.concatenate([ self.sensors, other.sensors @ R.T + t.T ]) return True def match_points(self, other): paired = [] for j, p2 in enumerate(other.points): for i, p1 in enumerate(self.points): dists1 = self.point_to_dists[tuple(p1)] dists2 = other.point_to_dists[tuple(p2)] common = dists1.intersection(dists2) if len(common) < 11: continue paired.append((i, j)) if len(paired) == 12: break if not paired: return None return np.stack(paired) def max_dist(self): best = 0 for s1 in self.sensors: for s2 in self.sensors: d = np.abs(s1 - s2).sum().item() best = max(d, best) return int(best) def parse_input(contents): sensors = contents.split('\n\n') output = [] for sensor in sensors: output.append( np.stack( [list(map(int, line.split(','))) for line in sensor.splitlines()[1:]] ) ) return output def part_1(sensors): sensors = [SensorData(np.copy(n)) for n in sensors] data = sensors[0] to_pair = set(range(1, len(sensors))) while to_pair: again = set() for i in to_pair: if data.add_points(sensors[i]): continue again.add(i) to_pair = again return data if __name__ == "__main__": print('--- Part 1 ---') with open('day19.txt') as f: sensors = parse_input(f.read()) data = part_1(sensors) print(data.points.shape[0]) print('--- Part 2 ---') print(data.max_dist())