multi-puzzle-solver 1.0.2__py3-none-any.whl → 1.0.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of multi-puzzle-solver might be problematic. Click here for more details.

@@ -0,0 +1,112 @@
1
+ import time
2
+ import numpy as np
3
+ from ortools.sat.python import cp_model
4
+ from ortools.sat.python.cp_model import LinearExpr as lxp
5
+
6
+ from puzzle_solver.core.utils import Pos, get_all_pos, get_char, get_next_pos, Direction
7
+
8
+
9
+ class Board:
10
+ def __init__(self, board: np.array, time_horizon: int = 10):
11
+ assert board.ndim == 2, f'board must be 2d, got {board.ndim}'
12
+ assert all(str(c.item()).isdecimal() for c in np.nditer(board)), 'board must contain only digits'
13
+ self.board = board
14
+ self.target_state = np.sort(board, axis=None).reshape(board.shape)
15
+ self.V, self.H = board.shape
16
+ self.min_value = int(np.min(board.flatten()))
17
+ self.max_value = int(np.max(board.flatten()))
18
+ self.time_horizon = time_horizon
19
+
20
+ self.model = cp_model.CpModel()
21
+ self.state: dict[tuple[Pos, int], cp_model.IntVar] = {}
22
+ self.decision: dict[int, dict[Pos, cp_model.IntVar]] = {t: {} for t in range(self.time_horizon - 1)}
23
+
24
+ self.create_vars()
25
+ self.add_all_constraints()
26
+ self.minimize_actions()
27
+ self.constrain_final_state()
28
+
29
+ def create_vars(self):
30
+ for pos in get_all_pos(self.V, self.H):
31
+ for t in range(self.time_horizon):
32
+ self.state[pos, t] = self.model.NewIntVar(self.min_value, self.max_value, f'state:{pos}:{t}')
33
+ for t in range(self.time_horizon - 1):
34
+ self.decision[t]['NOOP'] = self.model.NewBoolVar(f'decision:NOOP:{t}')
35
+ for pos in get_all_pos(self.V, self.H):
36
+ if pos.x == self.H - 1 or pos.y == self.V - 1:
37
+ continue
38
+ self.decision[t][pos] = self.model.NewBoolVar(f'decision:{pos}:{t}')
39
+
40
+ def add_all_constraints(self):
41
+ # one action at most every time
42
+ for decision_at_t in self.decision.values():
43
+ self.model.AddExactlyOne(list(decision_at_t.values()))
44
+ # constrain the state at t=0
45
+ for pos in get_all_pos(self.V, self.H):
46
+ self.model.Add(self.state[pos, 0] == get_char(self.board, pos))
47
+ # constrain the state dynamics at t=1..T
48
+ for action_pos in get_all_pos(self.V, self.H):
49
+ if action_pos.x == self.H - 1 or action_pos.y == self.V - 1:
50
+ continue
51
+ self.constrain_state(action_pos)
52
+ # state does not change if NOOP is chosen
53
+ for t in range(1, self.time_horizon):
54
+ noop_var = self.decision[t - 1]['NOOP']
55
+ for pos in get_all_pos(self.V, self.H):
56
+ self.model.Add(self.state[pos, t] == self.state[pos, t - 1]).OnlyEnforceIf(noop_var)
57
+
58
+ def constrain_state(self, action: Pos):
59
+ tl = action
60
+ tr = get_next_pos(tl, Direction.RIGHT)
61
+ bl = get_next_pos(tl, Direction.DOWN)
62
+ br = get_next_pos(tr, Direction.DOWN)
63
+ two_by_two = (tl, tr, br, bl)
64
+ # lock state outside the two by two
65
+ for pos in get_all_pos(self.V, self.H):
66
+ if pos in two_by_two:
67
+ continue
68
+ for t in range(1, self.time_horizon):
69
+ self.model.Add(self.state[pos, t] == self.state[pos, t - 1]).OnlyEnforceIf(self.decision[t - 1][action])
70
+ # rotate clockwise inside the two by two
71
+ clockwise = two_by_two[-1:] + two_by_two[:-1]
72
+ # print('action', action)
73
+ # print('two_by_two', two_by_two)
74
+ # print('clockwise', clockwise)
75
+ for pre_pos, post_pos in zip(clockwise, two_by_two):
76
+ for t in range(1, self.time_horizon):
77
+ # print(f'IF self.decision[{t - 1}][{action}] THEN self.state[{post_pos}, {t}] == self.state[{pre_pos}, {t - 1}]')
78
+ self.model.Add(self.state[post_pos, t] == self.state[pre_pos, t - 1]).OnlyEnforceIf(self.decision[t - 1][action])
79
+
80
+ def constrain_final_state(self):
81
+ final_time = self.time_horizon - 1
82
+ for pos in get_all_pos(self.V, self.H):
83
+ self.model.Add(self.state[pos, final_time] == get_char(self.target_state, pos))
84
+
85
+ def minimize_actions(self):
86
+ flat_decisions = [(var, t+1) for t, tvs in self.decision.items() for pos, var in tvs.items() if pos != 'NOOP']
87
+ self.model.Minimize(lxp.weighted_sum([p[0] for p in flat_decisions], [p[1] for p in flat_decisions]))
88
+
89
+ def solve_and_print(self, verbose: bool = True):
90
+ solver = cp_model.CpSolver()
91
+ tic = time.time()
92
+ solver.solve(self.model)
93
+ assignment: dict[Pos] = [None for _ in range(self.time_horizon - 1)]
94
+ if solver.StatusName() in ['OPTIMAL', 'FEASIBLE']:
95
+ for t, tvs in self.decision.items():
96
+ for pos, var in tvs.items():
97
+ if solver.Value(var) == 1:
98
+ assignment[t] = (pos.x, pos.y) if pos != 'NOOP' else 'NOOP'
99
+ for t in range(self.time_horizon):
100
+ res_at_t = np.full((self.V, self.H), ' ', dtype=object)
101
+ for pos in get_all_pos(self.V, self.H):
102
+ res_at_t[pos.y][pos.x] = solver.Value(self.state[pos, t])
103
+ print(f't={t}')
104
+ print(res_at_t)
105
+ if verbose:
106
+ print("Solution found:", assignment)
107
+ if verbose:
108
+ print("status:", solver.StatusName())
109
+ toc = time.time()
110
+ print(f"Time taken: {toc - tic:.2f} seconds")
111
+ return assignment
112
+