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.
- NUCS-0.1.dist-info/LICENSE.md +21 -0
- NUCS-0.1.dist-info/METADATA +78 -0
- NUCS-0.1.dist-info/RECORD +35 -0
- NUCS-0.1.dist-info/WHEEL +5 -0
- NUCS-0.1.dist-info/top_level.txt +1 -0
- nucs/__init__.py +0 -0
- nucs/heuristics/heuristic.py +24 -0
- nucs/heuristics/variable_heuristic.py +97 -0
- nucs/memory.py +73 -0
- nucs/problems/__init__.py +0 -0
- nucs/problems/alpha_problem.py +107 -0
- nucs/problems/bibd_problem.py +49 -0
- nucs/problems/golomb_problem.py +76 -0
- nucs/problems/problem.py +271 -0
- nucs/problems/queens_problem.py +22 -0
- nucs/problems/schur_lemma_problem.py +29 -0
- nucs/problems/sudoku_problem.py +46 -0
- nucs/propagators/__init__.py +0 -0
- nucs/propagators/affine_eq_propagator.py +55 -0
- nucs/propagators/affine_geq_propagator.py +52 -0
- nucs/propagators/affine_leq_propagator.py +52 -0
- nucs/propagators/alldifferent_propagator.py +180 -0
- nucs/propagators/count_eq_propagator.py +47 -0
- nucs/propagators/dummy_propagator.py +21 -0
- nucs/propagators/exactly_eq_propagator.py +44 -0
- nucs/propagators/lexicographic_leq_propagator.py +139 -0
- nucs/propagators/max_eq_propagator.py +36 -0
- nucs/propagators/max_leq_propagator.py +46 -0
- nucs/propagators/min_eq_propagator.py +36 -0
- nucs/propagators/min_geq_propagator.py +46 -0
- nucs/propagators/propagators.py +135 -0
- nucs/solvers/__init__.py +0 -0
- nucs/solvers/backtrack_solver.py +105 -0
- nucs/solvers/solver.py +41 -0
- nucs/statistics.py +44 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
from numba import njit # type: ignore
|
|
2
|
+
from numpy.typing import NDArray
|
|
3
|
+
|
|
4
|
+
from nucs.memory import END, MAX, MIN, START
|
|
5
|
+
from nucs.propagators import (
|
|
6
|
+
affine_eq_propagator,
|
|
7
|
+
affine_geq_propagator,
|
|
8
|
+
affine_leq_propagator,
|
|
9
|
+
alldifferent_propagator,
|
|
10
|
+
count_eq_propagator,
|
|
11
|
+
dummy_propagator,
|
|
12
|
+
exactly_eq_propagator,
|
|
13
|
+
lexicographic_leq_propagator,
|
|
14
|
+
max_eq_propagator,
|
|
15
|
+
max_leq_propagator,
|
|
16
|
+
min_eq_propagator,
|
|
17
|
+
min_geq_propagator,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
# The ordinals of the algorithms for all propagators (sorted by alphabetical ordering).
|
|
21
|
+
ALG_AFFINE_EQ = 0
|
|
22
|
+
ALG_AFFINE_GEQ = 1
|
|
23
|
+
ALG_AFFINE_LEQ = 2
|
|
24
|
+
ALG_ALLDIFFERENT = 3
|
|
25
|
+
ALG_COUNT_EQ = 4
|
|
26
|
+
ALG_DUMMY = 5
|
|
27
|
+
ALG_EXACTLY_EQ = 6
|
|
28
|
+
ALG_LEXICOGRAPHIC_LEQ = 7
|
|
29
|
+
ALG_MAX_EQ = 8
|
|
30
|
+
ALG_MAX_LEQ = 9
|
|
31
|
+
ALG_MIN_EQ = 10
|
|
32
|
+
ALG_MIN_GEQ = 11
|
|
33
|
+
|
|
34
|
+
PROPAGATOR_MODULES = [
|
|
35
|
+
affine_eq_propagator,
|
|
36
|
+
affine_geq_propagator,
|
|
37
|
+
affine_leq_propagator,
|
|
38
|
+
alldifferent_propagator,
|
|
39
|
+
count_eq_propagator,
|
|
40
|
+
dummy_propagator,
|
|
41
|
+
exactly_eq_propagator,
|
|
42
|
+
lexicographic_leq_propagator,
|
|
43
|
+
max_eq_propagator,
|
|
44
|
+
max_leq_propagator,
|
|
45
|
+
min_eq_propagator,
|
|
46
|
+
min_geq_propagator,
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def get_triggers(algorithm: int, size: int, data: NDArray) -> NDArray:
|
|
51
|
+
"""
|
|
52
|
+
Returns the triggers for a propagator.
|
|
53
|
+
:param algorithm: the ordinal of the algorithm
|
|
54
|
+
:param size: the number of variables
|
|
55
|
+
:return: an array of triggers
|
|
56
|
+
"""
|
|
57
|
+
return PROPAGATOR_MODULES[algorithm].get_triggers(size, data)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@njit("int64(uint8, int32[::1,:], int32[:])", cache=True)
|
|
61
|
+
def compute_domains(algorithm: int, domains: NDArray, data: NDArray) -> int:
|
|
62
|
+
"""
|
|
63
|
+
Computes the new domains for the variables.
|
|
64
|
+
:param algorithm: the ordinal of the algorithm
|
|
65
|
+
:param domains: the initial domains of the variables
|
|
66
|
+
:param data: the parameters of the propagator
|
|
67
|
+
:return: a status as an integer (INCONSISTENCY, CONSISTENCY, ENTAILMENT)
|
|
68
|
+
"""
|
|
69
|
+
if algorithm == ALG_AFFINE_EQ:
|
|
70
|
+
return affine_eq_propagator.compute_domains(domains, data)
|
|
71
|
+
if algorithm == ALG_AFFINE_GEQ:
|
|
72
|
+
return affine_geq_propagator.compute_domains(domains, data)
|
|
73
|
+
if algorithm == ALG_AFFINE_LEQ:
|
|
74
|
+
return affine_leq_propagator.compute_domains(domains, data)
|
|
75
|
+
if algorithm == ALG_ALLDIFFERENT:
|
|
76
|
+
return alldifferent_propagator.compute_domains(domains, data)
|
|
77
|
+
if algorithm == ALG_COUNT_EQ:
|
|
78
|
+
return count_eq_propagator.compute_domains(domains, data)
|
|
79
|
+
if algorithm == ALG_DUMMY:
|
|
80
|
+
return dummy_propagator.compute_domains(domains, data)
|
|
81
|
+
if algorithm == ALG_EXACTLY_EQ:
|
|
82
|
+
return exactly_eq_propagator.compute_domains(domains, data)
|
|
83
|
+
if algorithm == ALG_LEXICOGRAPHIC_LEQ:
|
|
84
|
+
return lexicographic_leq_propagator.compute_domains(domains, data)
|
|
85
|
+
if algorithm == ALG_MAX_EQ:
|
|
86
|
+
return max_eq_propagator.compute_domains(domains, data)
|
|
87
|
+
if algorithm == ALG_MAX_LEQ:
|
|
88
|
+
return max_leq_propagator.compute_domains(domains, data)
|
|
89
|
+
if algorithm == ALG_MIN_EQ:
|
|
90
|
+
return min_eq_propagator.compute_domains(domains, data)
|
|
91
|
+
return min_geq_propagator.compute_domains(domains, data)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@njit(cache=True)
|
|
95
|
+
def init_triggered_propagators(
|
|
96
|
+
triggered_propagators: NDArray,
|
|
97
|
+
entailed_propagators: NDArray,
|
|
98
|
+
shr_domain_changes: NDArray,
|
|
99
|
+
propagator_nb: int,
|
|
100
|
+
prop_var_bounds: NDArray,
|
|
101
|
+
prop_dom_indices: NDArray,
|
|
102
|
+
prop_triggers: NDArray,
|
|
103
|
+
) -> None:
|
|
104
|
+
triggered_propagators.fill(False)
|
|
105
|
+
for prop_idx in range(propagator_nb):
|
|
106
|
+
if not entailed_propagators[prop_idx]:
|
|
107
|
+
for var_idx in range(prop_var_bounds[prop_idx, START], prop_var_bounds[prop_idx, END]):
|
|
108
|
+
dom_idx = prop_dom_indices[var_idx]
|
|
109
|
+
if (shr_domain_changes[dom_idx, MIN] and prop_triggers[var_idx, MIN]) or (
|
|
110
|
+
shr_domain_changes[dom_idx, MAX] and prop_triggers[var_idx, MAX]
|
|
111
|
+
):
|
|
112
|
+
triggered_propagators[prop_idx] = True
|
|
113
|
+
break
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@njit(cache=True)
|
|
117
|
+
def update_triggered_propagators(
|
|
118
|
+
triggered_propagators: NDArray,
|
|
119
|
+
entailed_propagators: NDArray,
|
|
120
|
+
shr_domain_changes: NDArray,
|
|
121
|
+
propagator_nb: int,
|
|
122
|
+
prop_var_bounds: NDArray,
|
|
123
|
+
prop_dom_indices: NDArray,
|
|
124
|
+
prop_triggers: NDArray,
|
|
125
|
+
previous_prop_idx: int,
|
|
126
|
+
) -> None:
|
|
127
|
+
for prop_idx in range(propagator_nb):
|
|
128
|
+
if not entailed_propagators[prop_idx] and prop_idx != previous_prop_idx and not triggered_propagators[prop_idx]:
|
|
129
|
+
for var_idx in range(prop_var_bounds[prop_idx, START], prop_var_bounds[prop_idx, END]):
|
|
130
|
+
dom_idx = prop_dom_indices[var_idx]
|
|
131
|
+
if (shr_domain_changes[dom_idx, MIN] and prop_triggers[var_idx, MIN]) or (
|
|
132
|
+
shr_domain_changes[dom_idx, MAX] and prop_triggers[var_idx, MAX]
|
|
133
|
+
):
|
|
134
|
+
triggered_propagators[prop_idx] = True
|
|
135
|
+
break
|
nucs/solvers/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
from typing import Iterator, List, Optional
|
|
2
|
+
|
|
3
|
+
from nucs.heuristics.heuristic import Heuristic
|
|
4
|
+
from nucs.heuristics.variable_heuristic import (
|
|
5
|
+
VariableHeuristic,
|
|
6
|
+
first_not_instantiated_var_heuristic,
|
|
7
|
+
min_value_dom_heuristic,
|
|
8
|
+
)
|
|
9
|
+
from nucs.memory import new_domain_changes
|
|
10
|
+
from nucs.problems.problem import Problem, is_solved
|
|
11
|
+
from nucs.solvers.solver import Solver
|
|
12
|
+
from nucs.statistics import (
|
|
13
|
+
STATS_OPTIMIZER_SOLUTION_NB,
|
|
14
|
+
STATS_SOLVER_BACKTRACK_NB,
|
|
15
|
+
STATS_SOLVER_CHOICE_DEPTH,
|
|
16
|
+
STATS_SOLVER_CHOICE_NB,
|
|
17
|
+
STATS_SOLVER_SOLUTION_NB,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class BacktrackSolver(Solver):
|
|
22
|
+
"""
|
|
23
|
+
A solver relying on a backtracking mechanism.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
problem: Problem,
|
|
29
|
+
heuristic: Heuristic = VariableHeuristic(first_not_instantiated_var_heuristic, min_value_dom_heuristic),
|
|
30
|
+
):
|
|
31
|
+
super().__init__(problem)
|
|
32
|
+
self.choice_points = [] # type: ignore
|
|
33
|
+
self.shr_domain_changes = new_domain_changes(len(self.problem.shr_domains))
|
|
34
|
+
self.heuristic = heuristic
|
|
35
|
+
|
|
36
|
+
def solve(self) -> Iterator[List[int]]:
|
|
37
|
+
while True:
|
|
38
|
+
solution = self.solve_one()
|
|
39
|
+
if solution is None:
|
|
40
|
+
break
|
|
41
|
+
yield solution
|
|
42
|
+
if not self.backtrack():
|
|
43
|
+
break
|
|
44
|
+
|
|
45
|
+
def solve_one(self) -> Optional[List[int]]:
|
|
46
|
+
"""
|
|
47
|
+
Find at most one solution.
|
|
48
|
+
:return: the solution if it exists or None
|
|
49
|
+
"""
|
|
50
|
+
while self.filter():
|
|
51
|
+
if is_solved(self.problem.shr_domains):
|
|
52
|
+
self.problem.statistics[STATS_SOLVER_SOLUTION_NB] += 1
|
|
53
|
+
return self.problem.get_values() # problem is solved
|
|
54
|
+
self.heuristic.choose(
|
|
55
|
+
self.choice_points,
|
|
56
|
+
self.problem.shr_domains,
|
|
57
|
+
self.shr_domain_changes,
|
|
58
|
+
self.problem.dom_indices,
|
|
59
|
+
)
|
|
60
|
+
self.problem.statistics[STATS_SOLVER_CHOICE_NB] += 1
|
|
61
|
+
self.problem.statistics[STATS_SOLVER_CHOICE_DEPTH] = max(
|
|
62
|
+
self.problem.statistics[STATS_SOLVER_CHOICE_DEPTH], len(self.choice_points)
|
|
63
|
+
)
|
|
64
|
+
return None
|
|
65
|
+
|
|
66
|
+
def minimize(self, variable_idx: int) -> Optional[List[int]]:
|
|
67
|
+
solution = None
|
|
68
|
+
while (new_solution := self.solve_one()) is not None:
|
|
69
|
+
solution = new_solution
|
|
70
|
+
self.problem.statistics[STATS_OPTIMIZER_SOLUTION_NB] += 1
|
|
71
|
+
self.reset()
|
|
72
|
+
self.problem.set_max_value(variable_idx, solution[variable_idx] - 1)
|
|
73
|
+
return solution
|
|
74
|
+
|
|
75
|
+
def backtrack(self) -> bool:
|
|
76
|
+
"""
|
|
77
|
+
Backtracks and updates the problem's domains
|
|
78
|
+
:return: true iff it is possible to backtrack
|
|
79
|
+
"""
|
|
80
|
+
if len(self.choice_points) == 0:
|
|
81
|
+
return False
|
|
82
|
+
self.problem.statistics[STATS_SOLVER_BACKTRACK_NB] += 1
|
|
83
|
+
self.shr_domain_changes[:, :] = True
|
|
84
|
+
self.problem.shr_domains = self.choice_points.pop()
|
|
85
|
+
self.problem.reset_entailed_propagators()
|
|
86
|
+
return True
|
|
87
|
+
|
|
88
|
+
def filter(self) -> bool:
|
|
89
|
+
"""
|
|
90
|
+
Achieves bound consistency and backtracks if necessary.
|
|
91
|
+
:return: true iff bound consistency has been achieved
|
|
92
|
+
"""
|
|
93
|
+
while not self.problem.filter(self.shr_domain_changes):
|
|
94
|
+
if not self.backtrack():
|
|
95
|
+
return False
|
|
96
|
+
return True
|
|
97
|
+
|
|
98
|
+
def reset(self) -> None:
|
|
99
|
+
"""
|
|
100
|
+
Resets the solver by resetting the problem and the choice points.
|
|
101
|
+
"""
|
|
102
|
+
self.choice_points.clear()
|
|
103
|
+
self.shr_domain_changes[:, :] = True
|
|
104
|
+
self.problem.reset_shr_domains()
|
|
105
|
+
self.problem.reset_entailed_propagators()
|
nucs/solvers/solver.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
from typing import Iterator, List, Optional
|
|
2
|
+
|
|
3
|
+
from nucs.problems.problem import Problem
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class Solver:
|
|
7
|
+
"""
|
|
8
|
+
A solver.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
def __init__(self, problem: Problem):
|
|
12
|
+
self.problem = problem
|
|
13
|
+
|
|
14
|
+
def solve(self) -> Iterator[List[int]]: # type: ignore
|
|
15
|
+
"""
|
|
16
|
+
Returns an iterator over the solutions.
|
|
17
|
+
:return: an iterator
|
|
18
|
+
"""
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
def solve_all(self) -> List[List[int]]:
|
|
22
|
+
"""
|
|
23
|
+
Returns the list of all solutions.
|
|
24
|
+
:return: a list of list of integers
|
|
25
|
+
"""
|
|
26
|
+
return [s for s in self.solve()]
|
|
27
|
+
|
|
28
|
+
def find_all(self) -> None:
|
|
29
|
+
"""
|
|
30
|
+
Find all solutions.
|
|
31
|
+
"""
|
|
32
|
+
for _ in self.solve():
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
def minimize(self, var_idx: int) -> Optional[List[int]]: # type: ignore
|
|
36
|
+
"""
|
|
37
|
+
Finds, if it exists, the solution to the problem that minimizes a given variable.
|
|
38
|
+
:param variable_idx: the index of the variable
|
|
39
|
+
:return: the solution if it exists or None
|
|
40
|
+
"""
|
|
41
|
+
pass
|
nucs/statistics.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from numpy.typing import NDArray
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def statistics_init() -> NDArray:
|
|
6
|
+
"""
|
|
7
|
+
Inits a Numpy array for storing the statistics.
|
|
8
|
+
:return: a Numpy array
|
|
9
|
+
"""
|
|
10
|
+
return np.array([0] * STATS_MAX, dtype=np.int32, order="C")
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def statistics_print(stats: NDArray) -> None:
|
|
14
|
+
"""
|
|
15
|
+
Pretty-prints an array of statistics.
|
|
16
|
+
:param stats: a Numpy array of statistics
|
|
17
|
+
"""
|
|
18
|
+
print(
|
|
19
|
+
{
|
|
20
|
+
"OPTIMIZER_SOLUTION_NB": int(stats[STATS_OPTIMIZER_SOLUTION_NB]),
|
|
21
|
+
"PROBLEM_FILTERS_NB": int(stats[STATS_PROBLEM_FILTER_NB]),
|
|
22
|
+
"PROPAGATOR_ENTAILMENT_NB": int(stats[STATS_PROPAGATOR_ENTAILMENT_NB]),
|
|
23
|
+
"PROPAGATOR_FILTERS_NB": int(stats[STATS_PROPAGATOR_FILTER_NB]),
|
|
24
|
+
"PROPAGATOR_FILTERS_NO_CHANGE_NB": int(stats[STATS_PROPAGATOR_FILTER_NO_CHANGE_NB]),
|
|
25
|
+
"PROPAGATOR_INCONSISTENCY_NB": int(stats[STATS_PROPAGATOR_INCONSISTENCY_NB]),
|
|
26
|
+
"SOLVER_BACKTRACK_NB": int(stats[STATS_SOLVER_BACKTRACK_NB]),
|
|
27
|
+
"SOLVER_CHOICE_NB": int(stats[STATS_SOLVER_CHOICE_NB]),
|
|
28
|
+
"SOLVER_CHOICE_DEPTH": int(stats[STATS_SOLVER_CHOICE_DEPTH]),
|
|
29
|
+
"SOLVER_SOLUTION_NB": int(stats[STATS_SOLVER_SOLUTION_NB]),
|
|
30
|
+
}
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
STATS_MAX = 10
|
|
35
|
+
STATS_OPTIMIZER_SOLUTION_NB = 0
|
|
36
|
+
STATS_PROBLEM_FILTER_NB = 1
|
|
37
|
+
STATS_PROPAGATOR_ENTAILMENT_NB = 2
|
|
38
|
+
STATS_PROPAGATOR_FILTER_NB = 3
|
|
39
|
+
STATS_PROPAGATOR_FILTER_NO_CHANGE_NB = 4
|
|
40
|
+
STATS_PROPAGATOR_INCONSISTENCY_NB = 5
|
|
41
|
+
STATS_SOLVER_BACKTRACK_NB = 6
|
|
42
|
+
STATS_SOLVER_CHOICE_NB = 7
|
|
43
|
+
STATS_SOLVER_CHOICE_DEPTH = 8
|
|
44
|
+
STATS_SOLVER_SOLUTION_NB = 9
|