Skip to content

Commit

Permalink
add generalized pivot optimizer
Browse files Browse the repository at this point in the history
  • Loading branch information
jonadsimon committed Feb 23, 2022
1 parent c11dc9f commit be545e1
Show file tree
Hide file tree
Showing 7 changed files with 153 additions and 77 deletions.
79 changes: 79 additions & 0 deletions MiniZinc_scripts/parameterized_board_generator_pivot.mzn
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
enum Letter = { A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z }; % english letters in alphabetical order

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%% Variables whose values need to be passed in %%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

int: n; % board size
int: m; % number of words

int: max_len; % length of the longest word being passed in

array [1..m] of int: word_lens; % array giving the length of each word
array [1..m] of int: pivot_inds; % array giving the 0-index the pivot-letter
array [1..m,1..max_len] of Letter: words; % array whose rows are words, with excess length padded with other letters

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

array [1..n,1..n] of var Letter: board;

array [1..m] of var 1..n: word_pos_y; % values constrained by board size
array [1..m] of var 1..n: word_pos_x; % values constrained by board size
array [1..m] of var -1..1: word_delta_y; % values constrained by {-1,0,1}
array [1..m] of var -1..1: word_delta_x; % values constrained by {-1,0,1}


constraint forall(i in 1..m)(word_delta_y[i] != 0 \/ word_delta_x[i] != 0); % at least one of dy,dx must be nonzero

% Pivot helper functions
function int: num_letters_lt_pivot_idx(int: i) = pivot_inds[i];
function int: num_letters_gte_pivot_idx(int: i) = word_lens[i] - pivot_inds[i];

% For each word, enforce that its last letter lies within the board.
% Since its first letter also lies within the board, this implies that the entire word lies within the board, by convexity.
constraint forall(i in 1..m)(
% End-of-word constraints
word_pos_y[i]+word_delta_y[i]*(num_letters_gte_pivot_idx(i)-1) >= 1
/\ word_pos_y[i]+word_delta_y[i]*(num_letters_gte_pivot_idx(i)-1) <= n
/\ word_pos_x[i]+word_delta_x[i]*(num_letters_gte_pivot_idx(i)-1) >= 1
/\ word_pos_x[i]+word_delta_x[i]*(num_letters_gte_pivot_idx(i)-1) <= n
% Start-of-word constraints
/\ word_pos_y[i]-word_delta_y[i]*num_letters_lt_pivot_idx(i) >= 1
/\ word_pos_y[i]-word_delta_y[i]*num_letters_lt_pivot_idx(i) <= n
/\ word_pos_x[i]-word_delta_x[i]*num_letters_lt_pivot_idx(i) >= 1
/\ word_pos_x[i]-word_delta_x[i]*num_letters_lt_pivot_idx(i) <= n
);

% Link the words to the board by linking letters to board positions.
constraint forall(i in 1..m)(
forall(j in (-1*num_letters_lt_pivot_idx(i)+1)..num_letters_gte_pivot_idx(i))(
words[i,num_letters_lt_pivot_idx(i)+j] = board[word_pos_y[i] + word_delta_y[i]*(j-1), word_pos_x[i] + word_delta_x[i]*(j-1)]
)
);

% Search the variables in order of decreasing word length (words are passed in as shortest-to-longest).
% For each word first search the positions in min-to-max order (intuition: start from the left/top of the board then work your way in).
% Then search the orientations in horizontal-to-diagonalmax order (intuition: diagonal words interfere with more other words)

annotation pos_var_strat;
annotation pos_val_strat;

% Define position searches and orientation searches separately, then interleave them in reverse order.
solve :: seq_search([
if x = 1 then int_search([word_pos_y[i], word_pos_x[i]], pos_var_strat, pos_val_strat)
else int_search([word_delta_y[i], word_delta_x[i]], first_fail, indomain_median)
endif | i in reverse(1..m), x in 1..2])
satisfy;


% Function which outputs, for a given board position, whether there is a word which overlaps that position
function bool: square_is_covered(int: i, int: j) =
exists(k in 1..m)(
exists(l in (-1*num_letters_lt_pivot_idx(k)+1)..num_letters_gte_pivot_idx(k))(
(i == fix(word_pos_y[k]) + fix(word_delta_y[k])*(l-1)) /\ (j == fix(word_pos_x[k]) + fix(word_delta_x[k])*(l-1))
)
);

output [ if square_is_covered(i,j) then format(fix(board[i,j])) else "_" endif ++
if j == n then "\n" else " " endif | i,j in 1..n ] ++ ["\n"];
73 changes: 41 additions & 32 deletions make_puzzle_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,38 +157,48 @@ def get_most_common_letter_indexes(board_words):
return most_common_letter_idx


def get_spiral_indices(board_size):
def get_jiggled_word_centers(board_words, max_jiggle=0.15):
"""
Adapted from https://stackoverflow.com/a/398302/2562771
Note: goes down-->right-->up-->left due to change in indexing
Get the randomly-perturbed center of each word. Ok to perturb up to 0.15 away from the geometric center.
For default max_jiggle=0.15, this translates to the following possibilities for centers for common word lengths:
4 : xOxx, xxOx
5 : xxOxx
6 : xxOxxx, xxxOxx
7 : xxOxxxx, xxxOxxx, xxxxOxx
8 : xxxOxxxx, xxxxOxxx
9 : xxxOxxxxx, xxxxOxxxx, xxxxxOxxx
10 : xxxOxxxxxx, xxxxOxxxxx, xxxxxOxxxx, xxxxxxOxxx
"""
y = x = 0
dy, dx = 0, -1
inds = []
for i in range(board_size**2):
if (-board_size/2 < y <= board_size/2) and (-board_size/2 < x <= board_size/2):
# Figure out how to simplify these expressions down
y_shift, x_shift = int(np.floor(y+(board_size-1)/2))+1, int(np.floor(x+(board_size-1)/2))+1
inds.append((y_shift, x_shift))
if y == x or (y < 0 and y == -x) or (y > 0 and y == 1-x):
dy, dx = -dx, dy
y, x = y+dy, x+dx
return inds


def make_data_file(board_words, board_size, strategy, filepath="tmp/data.dzn"):
jiggled_centers = []
for word in board_words:
possible_centers = [idx for idx in range(len(word)) if abs((len(word)-1)/2 - idx)/len(word) <= max_jiggle]
jiggled_centers.append(random.sample(possible_centers, 1)[0])
return jiggled_centers


def make_data_file(board_words, board_size, strategy, pivot, filepath="tmp/data.dzn"):
"""Generated a temporary data.dzn file to pass to the minizinc script."""
ys, xs = zip(*get_spiral_indices(board_size))
with open(filepath, "w") as outfile:
outfile.write(f"n = {board_size};\n")
outfile.write(f"m = {len(board_words)};\n\n")
outfile.write(f"y_pos_map = [ {', '.join([str(y) for y in ys])} ];\n")
outfile.write(f"x_pos_map = [ {', '.join([str(x) for x in xs])} ];\n\n")
outfile.write(f"max_len = {max([len(word) for word in board_words])};\n")
outfile.write(f"word_lens = [ {', '.join([str(len(word)) for word in board_words])} ];\n")
outfile.write(f"max_freq_idx = [ {', '.join([str(idx) for idx in get_most_common_letter_indexes(board_words)])} ];\n")

if pivot == "start":
pivots = [0]*len(board_words)
elif pivot == "center":
pivots = [(len(word)-1) // 2 for word in board_words]
elif pivot == "max_freq":
pivots = get_most_common_letter_indexes(board_words)
elif pivot == "jiggle":
pivots = get_jiggled_word_centers(board_words)
else:
raise ValueError(f"Pivot type must be one of 'start', 'center', 'max_freq', 'jiggle'. Received value: 'f{pivot}'")
outfile.write(f"pivot_inds = [ {', '.join([str(idx) for idx in pivots])} ];\n")

# ADDING THE BUFFER LIKE THIS IS HACKY, MAKE IT CLEANER
outfile.write(f"words = [| {' | '.join([', '.join(word + 'A'*(len(board_words[-1])-len(word))) for word in board_words])} |];\n\n")
outfile.write(f"words = [| {' | '.join([', '.join(word + 'Z'*(len(board_words[-1])-len(word))) for word in board_words])} |];\n\n")

# Add the search strategy conditional on the input strategy
if strategy == "min":
pos_var_strat = "smallest"
Expand Down Expand Up @@ -305,7 +315,7 @@ def find_words_in_board(board, word_tuples):
return covered_up_words, doubled_up_words, flattened_deltas


def make_puzzle(topic, board_size, packing_constant, strategy, optimize_words, relatedness_cutoff, n_proc=4):
def make_puzzle(topic, board_size, packing_constant, strategy, pivot, optimize_words, relatedness_cutoff, n_proc=4):
word_tuples, hidden_word_tuple_dict = get_related_words(topic, relatedness_cutoff)
word_tuples = [wt for wt in word_tuples if len(wt.board) <= board_size]

Expand All @@ -325,7 +335,7 @@ def make_puzzle(topic, board_size, packing_constant, strategy, optimize_words, r
# Answer: write a separate file for each to avoid needed to juggle read/write times

board_found = False
max_retries = 15
max_retries = 10
retries = 0
timeout = 5
while retries < max_retries and not board_found:
Expand All @@ -334,13 +344,10 @@ def make_puzzle(topic, board_size, packing_constant, strategy, optimize_words, r
# Shuffle words separately for each run.
word_tuples_to_fit = reshuffle_words_to_fit(word_tuples_to_fit)
# Generate Minizinc data file to feed into the parameterizd script.
make_data_file([wt.board for wt in word_tuples_to_fit], board_size, strategy, f"tmp/data{i+1}.dzn")
make_data_file([wt.board for wt in word_tuples_to_fit], board_size, strategy, pivot, f"tmp/data{i+1}.dzn")

# Run the script
# cmd = ["/Applications/MiniZincIDE.app/Contents/Resources/minizinc", "--solver", "Chuffed", "MiniZinc_scripts/parameterized_board_generator.mzn"]
# cmd = ["/Applications/MiniZincIDE.app/Contents/Resources/minizinc", "--solver", "Chuffed", "MiniZinc_scripts/parameterized_board_generator_centered.mzn"]
# cmd = ["/Applications/MiniZincIDE.app/Contents/Resources/minizinc", "--solver", "Chuffed", "MiniZinc_scripts/parameterized_board_generator_centered_max_freq.mzn"]
cmd = ["/Applications/MiniZincIDE.app/Contents/Resources/minizinc", "--solver", "Chuffed", "MiniZinc_scripts/parameterized_board_generator_centered_spiral.mzn"]
cmd = ["/Applications/MiniZincIDE.app/Contents/Resources/minizinc", "--solver", "Chuffed", "MiniZinc_scripts/parameterized_board_generator_pivot.mzn"]
ps = [subprocess.Popen(cmd + [f"tmp/data{i+1}.dzn"], stdout=subprocess.PIPE) for i in range(n_proc)]

time.sleep(timeout)
Expand Down Expand Up @@ -449,6 +456,8 @@ def make_puzzle(topic, board_size, packing_constant, strategy, optimize_words, r
help="Ratio of total letters to # board squares (default=1.10)")
parser.add_argument("--strategy", type=str, default="median",
help="Search strategy to use, one of 'min', 'median', 'max' (default='median')")
parser.add_argument("--pivot", type=str, default="center",
help="Letter to pivot the words on during optimization, one of 'start', 'center', 'jiggle', 'max_freq' (default='center')")
parser.add_argument("--optimize-words", type=bool, default=False, action=argparse.BooleanOptionalAction,
help="Optimize the word distribution for letter-overlaps in advance (default=False)")
parser.add_argument("--relatedness-cutoff", type=float, default=0.45,
Expand All @@ -457,5 +466,5 @@ def make_puzzle(topic, board_size, packing_constant, strategy, optimize_words, r

# Can't find solutions for words: 'flamboyant', 'coffee', 'dishwasher'

random.seed(0)
make_puzzle(args.topic, args.board_size, args.packing_constant, args.strategy, args.optimize_words, args.relatedness_cutoff)
random.seed(1)
make_puzzle(args.topic, args.board_size, args.packing_constant, args.strategy, args.pivot, args.optimize_words, args.relatedness_cutoff)
34 changes: 17 additions & 17 deletions other.rtf
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,24 @@
\margl1440\margr1440\vieww12820\viewh11940\viewkind0
\pard\tx720\tx1440\tx2160\tx2880\tx3600\tx4320\tx5040\tx5760\tx6480\tx7200\tx7920\tx8640\pardirnatural\qc\partightenfactor0

\f0\fs32 \cf2 Topic: blond / blue-eyed / baby\
\f0\fs32 \cf2 Topic: science / bedtime / stories\
\
\
O A F H Y F L R I G S A M A M\
P S A D E Y E T U C Y X E S R\
B I D Y P P U P E H Y D D I K\
R A G G A I I N U I T A E R T\
D I B G Y D N A S L L A G E R\
R H L E R Y D D S D I L D N H\
O T A L E L N I I N O A E R T\
B E T D P R O L L A P M V I R\
A E R N A U L L A L S I O A I\
L T O A I C B A N O G N L B B\
R K M H D T H P K P N A M U H\
I S U T E F A I Y T U O P X T\
A A G O R P S L C S O U L O A\
F T B A L D O L L K Y N G M R\
G E N E W O M B A B Y D U M B\
S W E N P E E L S B O O K E A\
L A Y F F I J T W N T H W G M\
E R S K I L L T R U O H A A R\
V C A G C L F D O U I S T P W\
E S E L A O H R T L T H R M I\
L O O I L R L O E O E H O O T\
Y A C H P E R C R S L G P M E\
A T L A T I C E R A L P E E L\
D I I W E A Y R T F R A R N O\
D M M S A L B U P U L Y N T D\
I I A E T Y B T Y O N O P N R\
M S X T I U S A H T O I O E A\
H T Y M T V D N F I X C M R M\
N O V E L I O Y O L N I S O A\
T A L E P I C M O G T G S Y Y\
\
\
animal asia babe baby bairn bald \uc0\u739 beer\u739 birth blond brat buxom chick child curly cute daddy diaper doll dumb eyed fair fetus gene girl hair handle human india inuit issue kiddy labor lager lanky latin leggy loved mama mortal pallid \u739 pallor\u739 poland pouty puppy ragga sandy sexy soul spoil sprog task teeth treat womb young}
always annals attic bathos book cellar climax clock drama epic fable floor garret horary hour jiffy legend level life loft math midday minute moment movie myth nature news novel page piece plot recital record report saga scoop sixty skill sleep storey study tale thing time timist truth while witelo wrote yore}
11 changes: 4 additions & 7 deletions tmp/data1.dzn
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
n = 15;
m = 51;

y_pos_map = [ 8, 9, 9, 8, 7, 7, 7, 8, 9, 10, 10, 10, 10, 9, 8, 7, 6, 6, 6, 6, 6, 7, 8, 9, 10, 11, 11, 11, 11, 11, 11, 10, 9, 8, 7, 6, 5, 5, 5, 5, 5, 5, 5, 6, 7, 8, 9, 10, 11, 12, 12, 12, 12, 12, 12, 12, 12, 11, 10, 9, 8, 7, 6, 5, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 ];
x_pos_map = [ 8, 8, 9, 9, 9, 8, 7, 7, 7, 7, 8, 9, 10, 10, 10, 10, 10, 9, 8, 7, 6, 6, 6, 6, 6, 6, 7, 8, 9, 10, 11, 11, 11, 11, 11, 11, 11, 10, 9, 8, 7, 6, 5, 5, 5, 5, 5, 5, 5, 5, 6, 7, 8, 9, 10, 11, 12, 12, 12, 12, 12, 12, 12, 12, 12, 11, 10, 9, 8, 7, 6, 5, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1 ];
m = 50;

max_len = 6;
word_lens = [ 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6 ];
max_freq_idx = [ 1, 1, 2, 1, 0, 1, 2, 2, 3, 1, 1, 1, 2, 1, 3, 1, 2, 2, 2, 1, 3, 3, 2, 4, 1, 4, 3, 1, 2, 3, 4, 2, 3, 2, 1, 1, 1, 4, 2, 1, 3, 1, 5, 0, 1, 3, 3, 3, 5, 0, 1 ];
words = [| T, E, S, T, A, A | B, A, S, H, A, A | B, E, A, N, A, A | S, E, E, D, A, A | A, G, O, N, A, A | G, A, L, A, A, A | Q, U, A, L, A, A | K, H, A, T, A, A | C, O, L, A, A, A | M, I, L, K, A, A | M, I, L, L, A, A | B, E, E, R, A, A | C, O, R, N, A, A | C, A, K, E, A, A | S, O, D, A, A, A | F, E, T, E, A, A | C, H, A, I, A, A | T, R, E, E, A, A | B, R, E, W, A, A | J, A, V, A, A, A | W, I, N, E, A, A | C, O, K, E, A, A | P, L, A, N, T, A | C, O, C, O, A, A | R, E, V, E, L, A | F, E, R, I, A, A | U, M, B, E, R, A | P, A, R, T, Y, A | F, E, A, S, T, A | S, U, G, A, R, A | M, O, C, H, A, A | E, V, E, N, T, A | B, R, E, A, D, A | C, H, E, E, R, A | L, A, T, T, E, A | D, R, I, N, K, A | B, R, O, W, N, A | J, U, I, C, E, A | S, N, A, C, K, A | Y, E, M, E, N, A | D, E, C, A, F, A | E, A, S, T, E, R | F, I, E, S, T, A | A, C, I, D, I, C | V, E, N, I, C, E | P, A, R, A, D, E | B, A, N, A, N, A | A, F, F, A, I, R | P, E, R, S, I, A | A, F, R, I, C, A | C, A, N, C, E, R |];
word_lens = [ 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6 ];
pivot_inds = [ 1, 1, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 2, 2, 3, 2, 2, 3, 2, 2, 2, 2, 2, 3, 2, 3, 3 ];
words = [| S, A, G, A, Z, Z | T, I, M, E, Z, Z | P, A, G, E, Z, Z | L, O, F, T, Z, Z | Y, O, R, E, Z, Z | B, O, O, K, Z, Z | N, E, W, S, Z, Z | E, P, I, C, Z, Z | H, O, U, R, Z, Z | M, A, T, H, Z, Z | T, A, L, E, Z, Z | M, Y, T, H, Z, Z | P, L, O, T, Z, Z | L, I, F, E, Z, Z | C, L, O, C, K, Z | S, I, X, T, Y, Z | L, E, V, E, L, Z | S, T, U, D, Y, Z | P, I, E, C, E, Z | T, H, I, N, G, Z | D, R, A, M, A, Z | W, H, I, L, E, Z | F, L, O, O, R, Z | J, I, F, F, Y, Z | F, A, B, L, E, Z | N, O, V, E, L, Z | A, T, T, I, C, Z | W, R, O, T, E, Z | S, K, I, L, L, Z | S, C, O, O, P, Z | M, O, V, I, E, Z | T, R, U, T, H, Z | S, L, E, E, P, Z | B, A, T, H, O, S | N, A, T, U, R, E | H, O, R, A, R, Y | S, T, O, R, E, Y | M, I, D, D, A, Y | A, L, W, A, Y, S | M, O, M, E, N, T | R, E, P, O, R, T | C, L, I, M, A, X | A, N, N, A, L, S | W, I, T, E, L, O | L, E, G, E, N, D | C, E, L, L, A, R | R, E, C, O, R, D | T, I, M, I, S, T | G, A, R, R, E, T | M, I, N, U, T, E |];

pos_var_strat = first_fail;
pos_val_strat = indomain_median;
Loading

0 comments on commit be545e1

Please sign in to comment.