import numpy as np from collections import Counter, deque def initialize_potential(grid): return np.array(grid, dtype=float) def discrete_gradient(phi): gx = np.zeros_like(phi) gy = np.zeros_like(phi) gx[:-1, :] = phi[1:, :] - phi[:-1, :] gy[:, :-1] = phi[:, 1:] - phi[:, :-1] mag = np.sqrt(gx**2 + gy**2) return gx, gy, mag def dirichlet_energy(phi): _, _, mag = discrete_gradient(phi) return np.sum(mag**2) def sigma_l1(phi_current, phi_target): return np.sum(np.abs(phi_target - phi_current)) def tile_transform(phi_in, out_shape): h_in, w_in = phi_in.shape h_out, w_out = out_shape out = np.zeros(out_shape, dtype=float) for i in range(h_out): for j in range(w_out): out[i, j] = phi_in[i % h_in, j % w_in] return out def fill_enclosed(phi, boundary_mask=None): """ Fill zero regions that are fully enclosed by non-zero boundary. Returns a new array with enclosed holes filled with modal neighbor color. """ arr = np.array(phi, dtype=int) h, w = arr.shape if boundary_mask is None: boundary = (arr != 0) else: boundary = np.array(boundary_mask, dtype=bool) visited = np.zeros((h, w), dtype=bool) filled = arr.copy() def flood(start_r, start_c): q = deque() q.append((start_r, start_c)) comp = [] touches_border = False visited[start_r, start_c] = True while q: r, c = q.popleft() comp.append((r, c)) if r == 0 or c == 0 or r == h-1 or c == w-1: touches_border = True for dr, dc in ((1,0),(-1,0),(0,1),(0,-1)): nr, nc = r+dr, c+dc if 0 <= nr < h and 0 <= nc < w and not visited[nr, nc]: if arr[nr, nc] == 0: visited[nr, nc] = True q.append((nr, nc)) return comp, touches_border for i in range(h): for j in range(w): if arr[i, j] == 0 and not visited[i, j]: comp, touches_border = flood(i, j) if not touches_border: neighbor_vals = [] for (r, c) in comp: for dr, dc in ((1,0),(-1,0),(0,1),(0,-1)): nr, nc = r+dr, c+dc if 0 <= nr < h and 0 <= nc < w: v = arr[nr, nc] if v != 0: neighbor_vals.append(int(v)) if neighbor_vals: mode_color = Counter(neighbor_vals).most_common(1)[0][0] else: mode_color = 1 for (r, c) in comp: filled[r, c] = mode_color return filled class Transform: def __init__(self, func, name, params=None): self.func = func self.name = name self.params = params or {} def apply(self, phi): return self.func(phi, **self.params) def compose(self, other): def composed(phi): return other.apply(self.apply(phi)) return Transform(lambda p: composed(p), f"{self.name}∘{other.name}") def __repr__(self): return f"" def beam_minimize(phi_in, phi_target, atomic_library, beam_width=4, max_depth=4, lock_coeff=0.1): identity = Transform(lambda p: p, "Id") beam = [(identity, phi_in, 0.0)] best = None for depth in range(max_depth): candidates = [] for T_cur, _, _ in beam: for T_atomic in atomic_library: T_new = T_cur.compose(T_atomic) phi_new = T_new.apply(phi_in) residue = sigma_l1(phi_new, phi_target) energy = dirichlet_energy(phi_new) score = residue + lock_coeff * energy candidates.append((T_new, phi_new, score)) if not candidates: break candidates.sort(key=lambda x: x[2]) beam = candidates[:beam_width] best = beam[0] if sigma_l1(best[1], phi_target) == 0: break return best[0], best[1]