NUCS 0.1__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.
@@ -0,0 +1,271 @@
1
+ from typing import Tuple, Union
2
+
3
+ import numpy as np
4
+ from numba import njit # type: ignore
5
+ from numba.typed import List
6
+ from numpy.typing import NDArray
7
+
8
+ from nucs.memory import (
9
+ END,
10
+ MAX,
11
+ MIN,
12
+ PROP_ENTAILMENT,
13
+ PROP_INCONSISTENCY,
14
+ START,
15
+ new_algorithms,
16
+ new_bounds,
17
+ new_data,
18
+ new_domain_offsets_by_values,
19
+ new_domains_by_values,
20
+ new_entailed_propagators,
21
+ new_indices,
22
+ new_indices_by_values,
23
+ new_offsets,
24
+ new_triggered_propagators,
25
+ new_triggers,
26
+ )
27
+ from nucs.propagators.propagators import (
28
+ compute_domains,
29
+ get_triggers,
30
+ init_triggered_propagators,
31
+ update_triggered_propagators,
32
+ )
33
+ from nucs.statistics import (
34
+ STATS_PROBLEM_FILTER_NB,
35
+ STATS_PROPAGATOR_ENTAILMENT_NB,
36
+ STATS_PROPAGATOR_FILTER_NB,
37
+ STATS_PROPAGATOR_FILTER_NO_CHANGE_NB,
38
+ STATS_PROPAGATOR_INCONSISTENCY_NB,
39
+ statistics_init,
40
+ )
41
+
42
+
43
+ class Problem:
44
+ """
45
+ A problem is conceptually defined by a list of domains, a list of variables and a list of propagators.
46
+ A domain can be computed by applying an offset to a shared domain.
47
+ """
48
+
49
+ def __init__(self, shr_domains: List[Union[int, Tuple[int, int]]], dom_indices: List[int], dom_offsets: List[int]):
50
+ """
51
+ Inits the problem.
52
+ :param shr_domains: the shared domains expressed as a list
53
+ :param dom_indices: the domain indices expressed as a list
54
+ :param dom_offsets: the domain offsets expressed as a list
55
+ """
56
+ self.variable_nb = len(dom_indices)
57
+ self.shr_domains_backup = shr_domains # a copy of the shared domains (as a list) used when resetting a problem
58
+ self.shr_domains = new_domains_by_values(shr_domains)
59
+ self.dom_indices = new_indices_by_values(dom_indices)
60
+ self.dom_offsets = new_domain_offsets_by_values(dom_offsets)
61
+ self.statistics = statistics_init()
62
+
63
+ def reset_shr_domains(self) -> None:
64
+ """
65
+ Resets the shared domains to their initial values.
66
+ """
67
+ self.shr_domains = new_domains_by_values(self.shr_domains_backup)
68
+
69
+ def reset_entailed_propagators(self) -> None:
70
+ """
71
+ Marks all propagators as not entailed anymore.
72
+ """
73
+ self.entailed_propagators[:] = False
74
+
75
+ def set_propagators(self, propagators: List[Tuple[List[int], int, List[int]]]) -> None:
76
+ """
77
+ Completes the initialization of the problem by setting the propagators.
78
+ The order of the propagators has an impact on the global speed of the resolution:
79
+ it is recommended to put the more costly propagators at the end of the list.
80
+ :param propagators: the list of propagators as tuples of the form (list of variables, algorithm, data).
81
+ """
82
+ self.propagator_nb = len(propagators)
83
+ props_var_total_size = props_data_total_size = 0
84
+ self.triggered_propagators = new_triggered_propagators(self.propagator_nb)
85
+ self.entailed_propagators = new_entailed_propagators(self.propagator_nb)
86
+ self.algorithms = new_algorithms(self.propagator_nb)
87
+ # We will store propagator specific data in a global arrays, we need to compute variables and data bounds.
88
+ self.var_bounds = new_bounds(
89
+ self.propagator_nb
90
+ ) # there is a bit of redundancy here for faster access to the bounds
91
+ self.data_bounds = new_bounds(
92
+ self.propagator_nb
93
+ ) # there is a bit of redundancy here for faster access to the bounds
94
+ self.var_bounds[0, START] = self.data_bounds[0, START] = 0
95
+ for pidx, propagator in enumerate(propagators):
96
+ prop_var_size = len(propagator[0])
97
+ prop_data_size = len(propagator[2])
98
+ self.algorithms[pidx] = propagator[1]
99
+ if pidx > 0:
100
+ self.var_bounds[pidx, START] = self.var_bounds[pidx - 1, END]
101
+ self.data_bounds[pidx, START] = self.data_bounds[pidx - 1, END]
102
+ self.var_bounds[pidx, END] = self.var_bounds[pidx, START] + prop_var_size
103
+ self.data_bounds[pidx, END] = self.data_bounds[pidx, START] + prop_data_size
104
+ props_var_total_size += prop_var_size
105
+ props_data_total_size += prop_data_size
106
+ # Bounds have been computed and can now be used.
107
+ # The global arrays are the following:
108
+ self.props_dom_indices = new_indices(props_var_total_size)
109
+ self.props_dom_offsets = new_offsets(props_var_total_size)
110
+ self.props_triggers = new_triggers(props_var_total_size, False)
111
+ self.props_data = new_data(props_data_total_size)
112
+ # Let's init the global arrays.
113
+ for pidx, propagator in enumerate(propagators):
114
+ prop_vars = propagator[0]
115
+ self.props_dom_indices[self.var_bounds[pidx, START] : self.var_bounds[pidx, END]] = self.dom_indices[
116
+ prop_vars
117
+ ] # this is cached for faster access
118
+ self.props_dom_offsets[self.var_bounds[pidx, START] : self.var_bounds[pidx, END]] = self.dom_offsets[
119
+ prop_vars
120
+ ] # this is cached for faster access
121
+ self.props_data[self.data_bounds[pidx, START] : self.data_bounds[pidx, END]] = propagator[2]
122
+ self.props_triggers[self.var_bounds[pidx, START] : self.var_bounds[pidx, END]] = get_triggers(
123
+ propagator[1], len(propagator[0]), propagator[2]
124
+ )
125
+
126
+ def get_values(self) -> List[int]:
127
+ """
128
+ Gets the values for the variables (when instantiated).
129
+ :return: a list of integers
130
+ """
131
+ mins = self.shr_domains[self.dom_indices, MIN] + self.dom_offsets
132
+ return mins.tolist()
133
+
134
+ def set_min_value(self, var_idx: int, min_value: int) -> None:
135
+ """
136
+ Sets the minimal value of a variable.
137
+ :param var_idx: the index of the variable
138
+ :param min_value: the minimal value
139
+ """
140
+ self.shr_domains[(self.dom_indices[var_idx]), MIN] = min_value - self.dom_offsets[var_idx]
141
+
142
+ def set_max_value(self, var_idx: int, max_value: int) -> None:
143
+ """
144
+ Sets the maximal value of a variable.
145
+ :param var_idx: the index of the variable
146
+ :param min_value: the maximal value
147
+ """
148
+ self.shr_domains[(self.dom_indices[var_idx]), MAX] = max_value - self.dom_offsets[var_idx]
149
+
150
+ def __str__(self) -> str:
151
+ # TODO: fix this
152
+ return f"domains={self.shr_domains}"
153
+
154
+ def filter(self, shr_domain_changes: NDArray) -> bool:
155
+ """
156
+ Filters the problem's domains by applying the propagators until a fix point is reached.
157
+ :param shr_domain_changes: an array of shared domain changes
158
+ :return: False if the problem is not consistent
159
+ """
160
+ return filter(
161
+ self.statistics,
162
+ self.propagator_nb,
163
+ self.triggered_propagators,
164
+ self.entailed_propagators,
165
+ self.algorithms,
166
+ self.var_bounds,
167
+ self.data_bounds,
168
+ self.props_dom_indices,
169
+ self.props_dom_offsets,
170
+ self.props_triggers,
171
+ self.props_data,
172
+ self.shr_domains,
173
+ shr_domain_changes,
174
+ )
175
+
176
+ def pretty_print_solution(self, solution: List[int]) -> None:
177
+ """
178
+ Pretty prints a solution to the problem.
179
+ :param solution: a list of integers
180
+ """
181
+ print(solution)
182
+
183
+
184
+ @njit(cache=True)
185
+ def pop_propagator(propagator_queue: NDArray) -> int:
186
+ for prop_idx in range(len(propagator_queue)):
187
+ if propagator_queue[prop_idx]:
188
+ propagator_queue[prop_idx] = False
189
+ return prop_idx
190
+ return -1
191
+
192
+
193
+ @njit(cache=True)
194
+ def filter(
195
+ statistics: NDArray,
196
+ propagator_nb: int,
197
+ triggered_propagators: NDArray,
198
+ entailed_propagators: NDArray,
199
+ algorithms: NDArray,
200
+ var_bounds: NDArray,
201
+ data_bounds: NDArray,
202
+ props_indices: NDArray,
203
+ props_offsets: NDArray,
204
+ props_triggers: NDArray,
205
+ props_data: NDArray,
206
+ shr_domains: NDArray,
207
+ shr_domain_changes: NDArray,
208
+ ) -> bool:
209
+ """
210
+ Filters the problem's domains by applying the propagators until a fix point is reached.
211
+ :param shr_domain_changes: an array of shared domain changes
212
+ :return: False if the problem is not consistent
213
+ """
214
+ shr_domains_cur = np.empty_like(shr_domains)
215
+ statistics[STATS_PROBLEM_FILTER_NB] += 1
216
+ init_triggered_propagators(
217
+ triggered_propagators,
218
+ entailed_propagators,
219
+ shr_domain_changes,
220
+ propagator_nb,
221
+ var_bounds,
222
+ props_indices,
223
+ props_triggers,
224
+ )
225
+ while True:
226
+ prop_idx = pop_propagator(triggered_propagators)
227
+ if prop_idx == -1:
228
+ return True
229
+ statistics[STATS_PROPAGATOR_FILTER_NB] += 1
230
+ prop_var_start = var_bounds[prop_idx, START]
231
+ prop_var_end = var_bounds[prop_idx, END]
232
+ prop_indices = props_indices[prop_var_start:prop_var_end]
233
+ prop_offsets = props_offsets[prop_var_start:prop_var_end].reshape((-1, 1))
234
+ prop_domains = np.empty((2, len(prop_offsets)), dtype=np.int32).T # trick for order=F
235
+ np.add(shr_domains[prop_indices], prop_offsets, prop_domains)
236
+ status = compute_domains(
237
+ algorithms[prop_idx],
238
+ prop_domains,
239
+ props_data[data_bounds[prop_idx, START] : data_bounds[prop_idx, END]],
240
+ )
241
+ if status == PROP_INCONSISTENCY:
242
+ statistics[STATS_PROPAGATOR_INCONSISTENCY_NB] += 1
243
+ return False
244
+ if status == PROP_ENTAILMENT:
245
+ entailed_propagators[prop_idx] = True
246
+ statistics[STATS_PROPAGATOR_ENTAILMENT_NB] += 1
247
+ shr_domains_cur[:, :] = shr_domains
248
+ shr_domains[prop_indices] = prop_domains - prop_offsets
249
+ shr_domain_changes[:, :] = shr_domains_cur != shr_domains
250
+ if np.any(shr_domain_changes): # type: ignore
251
+ update_triggered_propagators(
252
+ triggered_propagators,
253
+ entailed_propagators,
254
+ shr_domain_changes,
255
+ propagator_nb,
256
+ var_bounds,
257
+ props_indices,
258
+ props_triggers,
259
+ prop_idx,
260
+ )
261
+ else:
262
+ statistics[STATS_PROPAGATOR_FILTER_NO_CHANGE_NB] += 1
263
+
264
+
265
+ @njit(cache=True)
266
+ def is_solved(shr_domains: NDArray) -> bool:
267
+ """
268
+ Returns true iff the problem is solved.
269
+ :return: a boolean
270
+ """
271
+ return bool(np.all(np.equal(shr_domains[:, MIN], shr_domains[:, MAX])))
@@ -0,0 +1,22 @@
1
+ from nucs.problems.problem import Problem
2
+ from nucs.propagators.propagators import ALG_ALLDIFFERENT
3
+
4
+
5
+ class QueensProblem(Problem):
6
+ """
7
+ A simple model for the n-queens problem.
8
+ """
9
+
10
+ def __init__(self, n: int):
11
+ super().__init__(
12
+ shr_domains=[(0, n - 1)] * n,
13
+ dom_indices=list(range(n)) * 3,
14
+ dom_offsets=[0] * n + list(range(n)) + list(range(0, -n, -1)),
15
+ )
16
+ self.set_propagators(
17
+ [
18
+ (list(range(n)), ALG_ALLDIFFERENT, []),
19
+ (list(range(n, 2 * n)), ALG_ALLDIFFERENT, []),
20
+ (list(range(2 * n, 3 * n)), ALG_ALLDIFFERENT, []),
21
+ ]
22
+ )
@@ -0,0 +1,29 @@
1
+ from nucs.problems.problem import Problem
2
+ from nucs.propagators.propagators import (
3
+ ALG_AFFINE_LEQ,
4
+ ALG_EXACTLY_EQ,
5
+ ALG_LEXICOGRAPHIC_LEQ,
6
+ )
7
+
8
+
9
+ class SchurLemmaProblem(Problem):
10
+ """
11
+ See https://www.csplib.org/Problems/prob015/.
12
+ """
13
+
14
+ def __init__(self, n: int) -> None:
15
+ super().__init__(shr_domains=[(0, 1)] * n * 3, dom_indices=list(range(n * 3)), dom_offsets=[0] * n * 3)
16
+ propagators = []
17
+ for x in range(n):
18
+ propagators.append(([x * 3, x * 3 + 1, x * 3 + 2], ALG_EXACTLY_EQ, [1, 1]))
19
+ for x in range(n):
20
+ for y in range(n):
21
+ z = (x + 1) + (y + 1) - 1
22
+ if 0 <= z < n:
23
+ for k in range(3):
24
+ propagators.append(([3 * x + k, 3 * y + k, 3 * z + k], ALG_AFFINE_LEQ, [1, 1, 1, 2]))
25
+ # breaking symmetries
26
+ propagators.append(
27
+ (list(range(0, n * 3, 3)) + list(range(1, n * 3, 3)) + list(range(2, n * 3, 3)), ALG_LEXICOGRAPHIC_LEQ, [])
28
+ )
29
+ self.set_propagators(propagators)
@@ -0,0 +1,46 @@
1
+ from typing import List, Tuple
2
+
3
+ from nucs.problems.problem import Problem
4
+ from nucs.propagators.propagators import ALG_ALLDIFFERENT
5
+
6
+
7
+ class SudokuProblem(Problem):
8
+ """
9
+ A simple model for the sudoku problem.
10
+ """
11
+
12
+ def __init__(self, givens: List[List[int]]):
13
+ super().__init__(
14
+ shr_domains=[(1, 9) if given == 0 else (given, given) for line in givens for given in line],
15
+ dom_indices=list(range(81)),
16
+ dom_offsets=[0] * 81,
17
+ )
18
+ propagators: List[Tuple[List[int], int, List[int]]] = []
19
+ for i in range(9):
20
+ propagators.append((list(range(0 + i * 9, 9 + i * 9)), ALG_ALLDIFFERENT, []))
21
+ propagators.append((list(range(0 + i, 81 + i, 9)), ALG_ALLDIFFERENT, []))
22
+ for i in range(3):
23
+ for j in range(3):
24
+ offset = i * 27 + j * 3
25
+ propagators.append(
26
+ (
27
+ [
28
+ 0 + offset,
29
+ 1 + offset,
30
+ 2 + offset,
31
+ 9 + offset,
32
+ 10 + offset,
33
+ 11 + offset,
34
+ 18 + offset,
35
+ 19 + offset,
36
+ 20 + offset,
37
+ ],
38
+ ALG_ALLDIFFERENT,
39
+ [],
40
+ )
41
+ )
42
+ self.set_propagators(propagators)
43
+
44
+ def pretty_print_solution(self, solution: List[int]) -> None:
45
+ for i in range(0, 81, 9):
46
+ print(solution[i : i + 9])
File without changes
@@ -0,0 +1,55 @@
1
+ import numpy as np
2
+ from numba import njit # type: ignore
3
+ from numpy.typing import NDArray
4
+
5
+ from nucs.memory import MAX, MIN, PROP_CONSISTENCY, PROP_INCONSISTENCY, new_triggers
6
+
7
+
8
+ def get_triggers(n: int, data: NDArray) -> NDArray:
9
+ """
10
+ This propagator is triggered whenever there is a change in the domain of a variable.
11
+ :param n: the number of variables
12
+ :return: an array of triggers
13
+ """
14
+ return new_triggers(n, True)
15
+
16
+
17
+ @njit(cache=True)
18
+ def compute_domain_sum(n: int, domains: NDArray, data: NDArray) -> NDArray:
19
+ domain_sum = np.empty(2, dtype=np.int32)
20
+ domain_sum[:] = data[-1]
21
+ for i in range(n):
22
+ c = data[i]
23
+ if c > 0:
24
+ domain_sum[MIN] -= domains[i, MAX] * c
25
+ domain_sum[MAX] -= domains[i, MIN] * c
26
+ elif c < 0:
27
+ domain_sum[MIN] -= domains[i, MIN] * c
28
+ domain_sum[MAX] -= domains[i, MAX] * c
29
+ return domain_sum
30
+
31
+
32
+ @njit("int64(int32[::1,:], int32[:])", cache=True)
33
+ def compute_domains(domains: NDArray, a: NDArray) -> int:
34
+ """
35
+ Implements Sigma_i a_i * x_i = a_{n-1}.
36
+ :param domains: the domains of the variables
37
+ :param a: the parameters of the propagator
38
+ """
39
+ n = len(domains)
40
+ domain_sum = compute_domain_sum(n, domains, a)
41
+ new_domains = np.empty_like(domains)
42
+ new_domains[:] = domains[:]
43
+ for i in range(n):
44
+ if a[i] > 0:
45
+ new_min = domains[i, MAX] - (domain_sum[MIN] // -a[i])
46
+ new_max = domains[i, MIN] + (domain_sum[MAX] // a[i])
47
+ elif a[i] < 0:
48
+ new_min = domains[i, MAX] - (-domain_sum[MAX] // a[i])
49
+ new_max = domains[i, MIN] + (-domain_sum[MIN] // -a[i])
50
+ new_domains[i, MIN] = max(domains[i, MIN], new_min)
51
+ new_domains[i, MAX] = min(domains[i, MAX], new_max)
52
+ if new_domains[i, MIN] > new_domains[i, MAX]:
53
+ return PROP_INCONSISTENCY
54
+ domains[:] = new_domains[:]
55
+ return PROP_CONSISTENCY
@@ -0,0 +1,52 @@
1
+ import numpy as np
2
+ from numba import njit # type: ignore
3
+ from numpy.typing import NDArray
4
+
5
+ from nucs.memory import (
6
+ MAX,
7
+ MIN,
8
+ PROP_CONSISTENCY,
9
+ PROP_ENTAILMENT,
10
+ PROP_INCONSISTENCY,
11
+ new_triggers,
12
+ )
13
+ from nucs.propagators.affine_eq_propagator import compute_domain_sum
14
+
15
+
16
+ def get_triggers(n: int, data: NDArray) -> NDArray:
17
+ """
18
+ Returns the triggers for this propagator.
19
+ :param n: the number of variables
20
+ :return: an array of triggers
21
+ """
22
+ triggers = new_triggers(n, False)
23
+ for i in range(n):
24
+ triggers[i, MIN] = data[i] < 0
25
+ triggers[i, MAX] = data[i] > 0
26
+ return triggers
27
+
28
+
29
+ @njit("int64(int32[::1,:], int32[:])", cache=True)
30
+ def compute_domains(domains: NDArray, a: NDArray) -> int:
31
+ """
32
+ Implements Sigma_i a_i * x_i >= a_{n-1}.
33
+ :param domains: the domains of the variables
34
+ :param a: the parameters of the propagator
35
+ """
36
+ n = len(domains)
37
+ domain_sum = compute_domain_sum(n, domains, a)
38
+ if domain_sum[MAX] <= 0:
39
+ return PROP_ENTAILMENT
40
+ new_domains = np.empty_like(domains)
41
+ new_domains[:] = domains[:]
42
+ for i in range(n):
43
+ if a[i] > 0:
44
+ new_min = domains[i, MAX] - (domain_sum[MIN] // -a[i])
45
+ new_domains[i, MIN] = max(domains[i, MIN], new_min)
46
+ elif a[i] < 0:
47
+ new_max = domains[i, MIN] + (-domain_sum[MIN] // -a[i])
48
+ new_domains[i, MAX] = min(domains[i, MAX], new_max)
49
+ if new_domains[i, MIN] > new_domains[i, MAX]:
50
+ return PROP_INCONSISTENCY
51
+ domains[:] = new_domains[:]
52
+ return PROP_CONSISTENCY
@@ -0,0 +1,52 @@
1
+ import numpy as np
2
+ from numba import njit # type: ignore
3
+ from numpy.typing import NDArray
4
+
5
+ from nucs.memory import (
6
+ MAX,
7
+ MIN,
8
+ PROP_CONSISTENCY,
9
+ PROP_ENTAILMENT,
10
+ PROP_INCONSISTENCY,
11
+ new_triggers,
12
+ )
13
+ from nucs.propagators.affine_eq_propagator import compute_domain_sum
14
+
15
+
16
+ def get_triggers(n: int, data: NDArray) -> NDArray:
17
+ """
18
+ Returns the triggers for this propagator.
19
+ :param n: the number of variables
20
+ :return: an array of triggers
21
+ """
22
+ triggers = new_triggers(n, False)
23
+ for i in range(n):
24
+ triggers[i, MIN] = data[i] > 0
25
+ triggers[i, MAX] = data[i] < 0
26
+ return triggers
27
+
28
+
29
+ @njit("int64(int32[::1,:], int32[:])", cache=True)
30
+ def compute_domains(domains: NDArray, a: NDArray) -> int:
31
+ """
32
+ Implements Sigma_i a_i * x_i <= a_{n-1}.
33
+ :param domains: the domains of the variables
34
+ :param a: the parameters of the propagator
35
+ """
36
+ n = len(domains)
37
+ domain_sum = compute_domain_sum(n, domains, a)
38
+ if domain_sum[MIN] >= 0:
39
+ return PROP_ENTAILMENT
40
+ new_domains = np.empty_like(domains)
41
+ new_domains[:] = domains[:]
42
+ for i in range(n):
43
+ if a[i] > 0:
44
+ new_max = domains[i, MIN] + (domain_sum[MAX] // a[i])
45
+ new_domains[i, MAX] = min(domains[i, MAX], new_max)
46
+ elif a[i] < 0:
47
+ new_min = domains[i, MAX] - (-domain_sum[MAX] // a[i])
48
+ new_domains[i, MIN] = max(domains[i, MIN], new_min)
49
+ if new_domains[i, MIN] > new_domains[i, MAX]:
50
+ return PROP_INCONSISTENCY
51
+ domains[:] = new_domains[:]
52
+ return PROP_CONSISTENCY