Skip to content

Commit

Permalink
Subgrid_fill (#18)
Browse files Browse the repository at this point in the history
* new fill algorithm

* Remove copy from fill algorithm

* new release version

* remove comment
  • Loading branch information
pmaher86 authored Oct 23, 2022
1 parent 0d87e83 commit 92780fa
Show file tree
Hide file tree
Showing 3 changed files with 62 additions and 24 deletions.
83 changes: 61 additions & 22 deletions blacksquare/crossword.py
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ def __deepcopy__(self, memo):
copied._words = copy.deepcopy(self._words)
for word in copied._words.values():
word._parent = copied
copied._dependency_graph = copy.deepcopy(self._dependency_graph)
return copied

def __repr__(self):
Expand Down Expand Up @@ -572,10 +573,26 @@ def get_disconnected_open_subgrids(self) -> List[List[Word]]:
sorted(list(cc)) for cc in nx.connected_components(self._dependency_graph)
]

def hashable_state(
self, word_indices: List[WordIndex]
) -> Tuple[Tuple[WordIndex, str], ...]:
"""Returns a list of tuple of (word index, current value) pairs in sorted order.
This provides a hashable object describing the state of the grid which can be
compared between different Crossword objects.
Args:
word_indices (List[WordIndex]): The list of word indices of interest.
Returns:
Tuple[Tuple[WordIndex, str], ...]: A tuple of (word index, value) tuples
"""
sorted_indices = sorted(word_indices)
return tuple((i, self[i].value) for i in sorted_indices)

def fill(
self,
word_list: Optional[WordList] = None,
timeout: Optional[int] = 30,
timeout: Optional[float] = 30.0,
temperature: float = 0,
) -> Optional[Crossword]:
"""Searches for a possible fill, and returns the result as a new Crossword
Expand All @@ -591,39 +608,61 @@ def fill(
Optional[Crossword]: The filled Crossword. Returns None if the search is
exhausted or the timeout is hit.
"""
dead_end_states = set()
subgraphs = self.get_disconnected_open_subgrids()
start_time = time.time()
word_list = word_list if word_list is not None else self.word_list

def recurse_solve(xw: Crossword, display_context: Live) -> Optional[Crossword]:
open_words = [w for w in xw.iterwords() if w.is_open()]
if len(open_words) == 0:
return xw
num_matches = np.array([len(word_list.find_matches(w)) for w in open_words])
xw = self.copy()

def recurse_subgraph_fill(
active_subgraph: List[WordIndex], display_context: Live
) -> bool:
if xw.hashable_state(active_subgraph) in dead_end_states:
return False
num_matches = np.array(
[len(word_list.find_matches(xw[i])) for i in active_subgraph]
)
noise = np.abs(np.random.normal(scale=num_matches)) * temperature
word_to_match = open_words[np.argmin(num_matches + noise)]
word_to_match: Word = xw[active_subgraph[np.argmin(num_matches + noise)]]
matches = word_to_match.find_matches(word_list)
if len(matches) == 0:
return
if not matches:
dead_end_states.add(xw.hashable_state(active_subgraph))
return False
else:
noisy_matches = matches.rescore(
lambda _, s: s * np.random.lognormal(0.0, 0.1 * temperature)
)
new_xw = copy.deepcopy(xw)
old_value = word_to_match.value
# temp fill for subgraph calculation
xw[word_to_match.index] = noisy_matches.words[0]
new_subgraphs = [
s
for s in xw.get_disconnected_open_subgrids()
if set(s).issubset(set(active_subgraph))
]
for match in noisy_matches.words:
if timeout and time.time() > start_time + timeout:
return
new_xw[word_to_match.index] = match
display_context.update(new_xw._text_grid())
fill = recurse_solve(new_xw, live)
if fill:
return fill
xw[word_to_match.index] = old_value
return False
xw[word_to_match.index] = match
display_context.update(xw._text_grid())

for new_subgraph in sorted(new_subgraphs, key=len):
if not recurse_subgraph_fill(new_subgraph, display_context):
break
else:
return True
xw[word_to_match.index] = old_value
return False

with Live(self._text_grid(), refresh_per_second=4, transient=True) as live:
solution = recurse_solve(self, live)
if solution is not None:
live.update(solution._text_grid(), refresh=True)

return solution
for subgraph in sorted(subgraphs, key=len):
if recurse_subgraph_fill(subgraph, live):
live.update(xw._text_grid(), refresh=True)
else:
return
else:
return xw

def _text_grid(self, numbers: bool = False) -> Table:
"""Returns a rich Table that displays the crossword.
Expand Down
1 change: 0 additions & 1 deletion blacksquare/word_list.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,6 @@ def find_matches(self, word: Word) -> MatchWordList:
"""
return self.find_matches_str(word.value)

# TODO: lru cache?
@lru_cache(128)
def find_matches_str(self, query: str) -> MatchWordList:
"""Find matches for a query string. Open letters can be represented by a " ",
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[metadata]
name = blacksquare
version = 0.4.0
version = 0.5.0
author = Patrick Maher
author_email = [email protected]
description = "A package for creating crossword puzzles"
Expand Down

0 comments on commit 92780fa

Please sign in to comment.