Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- # Adapted from https://stackoverflow.com/a/47934736
- import math
- import copy
- import subprocess
- import time
- import numpy as np
- import matplotlib.pyplot as plt # plotting-only
- import seaborn as sns # plotting-only
- np.set_printoptions(linewidth=120) # more nice console-output
- # 3x3 breaks but whatever
- M, N = 29, 29
- # T-shaped tetromino (rotations)
- T0 = np.array([[1, 1, 1],
- [0, 1, 0]])
- T90 = np.array([[0, 1],
- [1, 1],
- [0, 1]])
- T180 = np.array([[0, 1, 0],
- [1, 1, 1]])
- T270 = np.array([[1, 0],
- [1, 1],
- [1, 0]])
- polyominos = [T0, T90, T180, T270]
- def create_inverted_circle(N):
- if N % 2 == 0:
- raise ValueError("N must be an odd integer")
- grid = [[0 for _ in range(N)] for _ in range(N)]
- center =(int) ((N - 1) / 2.0) # Grid center coordinate
- radius = N / 2.0 # Adjust radius to ensure symmetry
- for x in range(N):
- for y in range(N):
- # Calculate distance from cell center to grid center
- distance = math.sqrt((x - center)**2 + (y - center)**2)
- if distance >= radius:
- grid[x][y] = 1
- grid[center][center] = 1
- return np.array(grid)
- inverted_circle_grid = create_inverted_circle(N)
- """ Preprocessing
- Calculate:
- A: possible placements
- B: covered positions
- C: collisions between placements
- """
- placements = []
- covered = []
- for p_ind, p in enumerate(polyominos):
- mP, nP = p.shape
- for x in range(M):
- for y in range(N):
- if x + mP <= M and y + nP <= N:
- # Check if the placement is valid (not overlapping with cells set to 1)
- placement_valid = True
- for i in range(mP):
- for j in range(nP):
- if p[i,j] == 1 and inverted_circle_grid[x+i,y+j] == 1:
- placement_valid = False
- break
- if not placement_valid:
- break
- if placement_valid:
- placements.append((p_ind, x, y))
- cover = np.zeros((M, N), dtype=bool)
- cover[x:x+mP, y:y+nP] = p
- covered.append(cover)
- covered = np.array(covered)
- collisions = []
- for m in range(M):
- for n in range(N):
- collision_set = np.flatnonzero(covered[:, m, n])
- collisions.append(collision_set)
- print(f"Placements: {len(placements)}, Collision sets: {len(collisions)}")
- def parse_solution(output):
- # assumes there is one
- vars = []
- for line in output.split("\n"):
- if line:
- if line[0] == 'v':
- line_vars = list(map(lambda x: int(x), line.split()[1:]))
- vars.extend(line_vars)
- return vars
- def solve(CNF):
- p = subprocess.Popen(["cryptominisat5.exe"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True, encoding='utf-8')
- result = p.communicate(input=CNF)[0]
- sat_line = result.find('s SATISFIABLE')
- if sat_line != -1:
- # solution found!
- vars = parse_solution(result)
- return True, vars
- else:
- return False, None
- """ Helper-function: Cardinality constraints """
- # K-ARY CONSTRAINT GENERATION
- # ###########################
- # SINZ, Carsten. Towards an optimal CNF encoding of boolean cardinality constraints.
- # CP, 2005, 3709. Jg., S. 827-831.
- def next_var_index(start):
- next_var = start
- while(True):
- yield next_var
- next_var += 1
- class s_index():
- def __init__(self, start_index):
- self.firstEnvVar = start_index
- def next(self,i,j,k):
- return self.firstEnvVar + i*k +j
- def gen_seq_circuit(k, input_indices, next_var_index_gen):
- cnf_string = ''
- s_index_gen = s_index(next(next_var_index_gen))
- if len(input_indices) < 3:
- # Handle cases with fewer than 3 input indices
- for i in range(len(input_indices)):
- cnf_string += (str(-input_indices[i]) + ' ' + str(s_index_gen.next(0, i, k)) + ' 0\n')
- return (cnf_string, len(input_indices) * k, 2 * len(input_indices) * k + len(input_indices) - 3 * k - 1)
- # write clauses of first partial sum (i.e. i=0)
- cnf_string += (str(-input_indices[0]) + ' ' + str(s_index_gen.next(0, 0, k)) + ' 0\n')
- for i in range(1, k):
- cnf_string += (str(-s_index_gen.next(0, i, k)) + ' 0\n')
- # write clauses for general case (i.e. 0 < i < n-1)
- for i in range(1, len(input_indices) - 1):
- cnf_string += (str(-input_indices[i]) + ' ' + str(s_index_gen.next(i, 0, k)) + ' 0\n')
- cnf_string += (str(-s_index_gen.next(i - 1, 0, k)) + ' ' + str(s_index_gen.next(i, 0, k)) + ' 0\n')
- for u in range(1, k):
- cnf_string += (str(-input_indices[i]) + ' ' + str(-s_index_gen.next(i - 1, u - 1, k)) + ' ' + str(s_index_gen.next(i, u, k)) + ' 0\n')
- cnf_string += (str(-s_index_gen.next(i - 1, u, k)) + ' ' + str(s_index_gen.next(i, u, k)) + ' 0\n')
- cnf_string += (str(-input_indices[i]) + ' ' + str(-s_index_gen.next(i - 1, k - 1, k)) + ' 0\n')
- # last clause for last variable
- cnf_string += (str(-input_indices[-1]) + ' ' + str(-s_index_gen.next(len(input_indices) - 2, k - 1, k)) + ' 0\n')
- return (cnf_string, (len(input_indices) - 1) * k, 2 * len(input_indices) * k + len(input_indices) - 3 * k - 1)
- def gen_at_most_n_constraints(vars, start_var, n):
- constraint_string = ''
- used_clauses = 0
- used_vars = 0
- index_gen = next_var_index(start_var)
- circuit = gen_seq_circuit(n, vars, index_gen)
- constraint_string += circuit[0]
- used_clauses += circuit[2]
- used_vars += circuit[1]
- start_var += circuit[1]
- return [constraint_string, used_clauses, used_vars, start_var]
- """ SAT-CNF: BASE """
- X = np.arange(1, len(placements)+1) # decision-vars
- # 1-index for CNF
- Y = np.arange(len(placements)+1, len(placements)+1 + M*N).reshape(M,N)
- next_var = len(placements)+1 + M*N # aux-var gen
- n_clauses = 0
- cnf = '' # slow string appends
- # int-based would be better
- # <= 1 for each collision-set
- for cset in collisions:
- constraint_string, used_clauses, used_vars, next_var = \
- gen_at_most_n_constraints(X[cset].tolist(), next_var, 1)
- n_clauses += used_clauses
- cnf += constraint_string
- # if field marked: one of covering placements active
- for x in range(M):
- for y in range(N):
- covering_placements = X[np.flatnonzero(covered[:, x, y])] # could reuse collisions
- clause = str(-Y[x,y])
- for i in covering_placements:
- clause += ' ' + str(i)
- clause += ' 0\n'
- cnf += clause
- n_clauses += 1
- print('BASE CNF size')
- print('clauses: ', n_clauses)
- print('vars: ', next_var - 1)
- """ SOLVE in loop -> decrease number of placed-fields until SAT """
- print('CORE LOOP')
- # Calculate area inside circle (non-inverted cells)
- circle_area = np.sum(inverted_circle_grid == 0)
- # Calculate minimum polyomino area
- min_poly_area = min(np.sum(p) for p in polyominos)
- # Floor to nearest multiple of minimum polyomino area
- N_FIELD_HIT = (circle_area // min_poly_area) * min_poly_area
- max_possible = (int)(N_FIELD_HIT/min_poly_area)
- time_start = time.time()
- while True:
- print(' N_FIELDS >= ', N_FIELD_HIT)
- # sum(y) >= N_FIELD_HIT
- # == sum(not y) <= M*N - N_FIELD_HIT
- cnf_final = copy.copy(cnf)
- n_clauses_final = n_clauses
- if N_FIELD_HIT == M*N: # awkward special case
- constraint_string = ''.join([str(y) + ' 0\n' for y in Y.ravel()])
- n_clauses_final += N_FIELD_HIT
- else:
- constraint_string, used_clauses, used_vars, next_var = \
- gen_at_most_n_constraints((-Y).ravel().tolist(), next_var, M*N - N_FIELD_HIT)
- n_clauses_final += used_clauses
- n_vars_final = next_var - 1
- cnf_final += constraint_string
- cnf_final = 'p cnf ' + str(n_vars_final) + ' ' + str(n_clauses) + \
- ' \n' + cnf_final # header
- status, sol = solve(cnf_final)
- if status:
- print(' SOL found: ', N_FIELD_HIT)
- """ Print sol """
- res = np.full((M, N), -1) # Initialize with -1 for 'X'
- # Replace -1 with 0 where inverted_circle_grid is 0
- res[inverted_circle_grid == 0] = 0
- # Create mapping of polyomino types to list of available numbers
- poly_numbers = {}
- current_num = 1
- import random
- # Count placements and create a shuffled range of numbers
- n_placements = sum(1 for v in sol[:X.shape[0]] if v > 0)
- numbers = list(range(1, n_placements + 1))
- random.shuffle(numbers)
- # Create mapping of placement index to number
- number_map = {}
- number_idx = 0
- # First pass: assign numbers to placements
- for i, v in enumerate(sol[:X.shape[0]]):
- if v > 0:
- number_map[i] = numbers[number_idx]
- number_idx += 1
- # Second pass: fill the result grid
- counter = n_placements + 1
- for i, v in enumerate(sol[:X.shape[0]]):
- if v > 0:
- p, x, y = placements[i]
- pM, pN = polyominos[p].shape
- poly_nnz = np.where(polyominos[p] != 0)
- x_inds, y_inds = x+poly_nnz[0], y+poly_nnz[1]
- res[x_inds, y_inds] = number_map[i]
- print(res)
- """ Plot """
- ax1 = plt.subplot2grid((5, 12), (0, 0), colspan=11, rowspan=5)
- mask = (res==0)
- # Create custom colormap with black for -1, white for 0, and colors for others
- from matplotlib.colors import ListedColormap
- colors = ['black', 'white'] + sns.color_palette('gist_rainbow', n_colors=counter)
- custom_cmap = ListedColormap(colors)
- sns.heatmap(res, cmap=custom_cmap, mask=mask, cbar=False, square=True, linewidths=.1, ax=ax1, vmin=-1, vmax=counter-1)
- elapsed_time = time.time() - time_start
- plt.title(f"{M}x{N} Placements: {sum(1 for v in sol[:X.shape[0]] if v > 0)}/{max_possible} ({elapsed_time:.2f}s)")
- plt.tight_layout()
- plt.show()
- break
- with open(f"output_{M}X{N}.txt",mode="w") as file:
- file.write(cnf_final)
- N_FIELD_HIT -= min_poly_area # binary-search could be viable in some cases
- # but beware the empirical asymmetry in SAT-solvers:
- # finding solution vs. proving there is none!
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement