co2114 2026.1.1__tar.gz → 2026.1.3__tar.gz
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.
- {co2114-2026.1.1 → co2114-2026.1.3}/PKG-INFO +1 -1
- {co2114-2026.1.1 → co2114-2026.1.3}/co2114/agent/environment.py +1 -1
- co2114-2026.1.3/co2114/constraints/csp/__init__.py +429 -0
- co2114-2026.1.3/co2114/constraints/csp/util.py +128 -0
- co2114-2026.1.3/co2114/constraints/sudoku.py +191 -0
- co2114-2026.1.3/co2114/optimisation/__init__.py +1 -0
- co2114-2026.1.3/co2114/optimisation/adversarial.py +387 -0
- {co2114-2026.1.1 → co2114-2026.1.3}/co2114/optimisation/planning.py +125 -41
- {co2114-2026.1.1 → co2114-2026.1.3}/co2114/optimisation/things.py +9 -0
- {co2114-2026.1.1 → co2114-2026.1.3}/co2114.egg-info/PKG-INFO +1 -1
- {co2114-2026.1.1 → co2114-2026.1.3}/co2114.egg-info/SOURCES.txt +1 -1
- {co2114-2026.1.1 → co2114-2026.1.3}/setup.py +1 -1
- co2114-2026.1.1/co2114/constraints/csp/__init__.py +0 -262
- co2114-2026.1.1/co2114/constraints/csp/util.py +0 -89
- co2114-2026.1.1/co2114/constraints/sudoku.py +0 -113
- co2114-2026.1.1/co2114/optimisation/__init__.py +0 -1
- co2114-2026.1.1/co2114/optimisation/minimax.py +0 -185
- {co2114-2026.1.1 → co2114-2026.1.3}/LICENSE +0 -0
- {co2114-2026.1.1 → co2114-2026.1.3}/README.md +0 -0
- {co2114-2026.1.1 → co2114-2026.1.3}/co2114/__init__.py +0 -0
- {co2114-2026.1.1 → co2114-2026.1.3}/co2114/agent/__init__.py +0 -0
- {co2114-2026.1.1 → co2114-2026.1.3}/co2114/agent/things.py +0 -0
- {co2114-2026.1.1 → co2114-2026.1.3}/co2114/constraints/__init__.py +0 -0
- {co2114-2026.1.1 → co2114-2026.1.3}/co2114/constraints/magic.py +0 -0
- {co2114-2026.1.1 → co2114-2026.1.3}/co2114/engine.py +0 -0
- {co2114-2026.1.1 → co2114-2026.1.3}/co2114/reasoning/__init__.py +0 -0
- {co2114-2026.1.1 → co2114-2026.1.3}/co2114/reasoning/cluedo.py +0 -0
- {co2114-2026.1.1 → co2114-2026.1.3}/co2114/reasoning/inference.py +0 -0
- {co2114-2026.1.1 → co2114-2026.1.3}/co2114/reasoning/logic.py +0 -0
- {co2114-2026.1.1 → co2114-2026.1.3}/co2114/search/__init__.py +0 -0
- {co2114-2026.1.1 → co2114-2026.1.3}/co2114/search/graph.py +0 -0
- {co2114-2026.1.1 → co2114-2026.1.3}/co2114/search/maze.py +0 -0
- {co2114-2026.1.1 → co2114-2026.1.3}/co2114/search/things.py +0 -0
- {co2114-2026.1.1 → co2114-2026.1.3}/co2114/search/util.py +0 -0
- {co2114-2026.1.1 → co2114-2026.1.3}/co2114/util/__init__.py +0 -0
- {co2114-2026.1.1 → co2114-2026.1.3}/co2114/util/colours.py +0 -0
- {co2114-2026.1.1 → co2114-2026.1.3}/co2114/util/fonts.py +0 -0
- {co2114-2026.1.1 → co2114-2026.1.3}/co2114.egg-info/dependency_links.txt +0 -0
- {co2114-2026.1.1 → co2114-2026.1.3}/co2114.egg-info/requires.txt +0 -0
- {co2114-2026.1.1 → co2114-2026.1.3}/co2114.egg-info/top_level.txt +0 -0
- {co2114-2026.1.1 → co2114-2026.1.3}/setup.cfg +0 -0
|
@@ -66,7 +66,7 @@ class BaseEnvironment:
|
|
|
66
66
|
|
|
67
67
|
for i in range(steps):
|
|
68
68
|
self.__counter += 1
|
|
69
|
-
if self.is_done: # print
|
|
69
|
+
if self.is_done: # print termination message and exit
|
|
70
70
|
print(f"{self}: Simulation complete after {i} of {steps} iterations.")
|
|
71
71
|
return
|
|
72
72
|
self.step() # else iterate one step
|
|
@@ -0,0 +1,429 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import random
|
|
3
|
+
from ...agent.things import Thing, Agent
|
|
4
|
+
from ...agent.environment import Environment
|
|
5
|
+
from collections.abc import Callable, Iterable
|
|
6
|
+
from copy import deepcopy
|
|
7
|
+
|
|
8
|
+
from typing import Literal, override, TypeVar, Collection, Generic, Iterator
|
|
9
|
+
|
|
10
|
+
from .util import aslist
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
'util',
|
|
14
|
+
'CSPAgent',
|
|
15
|
+
'ConstraintSatisfactionProblem',
|
|
16
|
+
'CSPRunnerEnvironment',
|
|
17
|
+
'Variable',
|
|
18
|
+
'Factor']
|
|
19
|
+
|
|
20
|
+
# === CLASSES === #
|
|
21
|
+
Type = TypeVar("Type")
|
|
22
|
+
Domain = Collection[Type]
|
|
23
|
+
|
|
24
|
+
class __Variable(Thing, Generic[Type]):
|
|
25
|
+
""" Abstract Base Class for Variables in a CSP.
|
|
26
|
+
|
|
27
|
+
Defines operations on variables to allow arithmetic and comparison
|
|
28
|
+
"""
|
|
29
|
+
@property
|
|
30
|
+
def value(self) -> Type:
|
|
31
|
+
""" The value assigned to the variable (read only) """
|
|
32
|
+
return self.__value
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def is_assigned(self) -> bool:
|
|
36
|
+
""" Whether the variable has been assigned a value. """
|
|
37
|
+
return hasattr(self, '__value') and self.__value is not None
|
|
38
|
+
|
|
39
|
+
@override
|
|
40
|
+
def __eq__(self, x:Type) -> bool:
|
|
41
|
+
""" Equality comparison operator override. """
|
|
42
|
+
return (x == self.value)
|
|
43
|
+
|
|
44
|
+
@override
|
|
45
|
+
def __ne__(self, x:Type) -> bool:
|
|
46
|
+
""" Inequality comparison operator override. """
|
|
47
|
+
return (x != self.value)
|
|
48
|
+
|
|
49
|
+
@override
|
|
50
|
+
def __lt__(self, x:Type) -> bool:
|
|
51
|
+
""" Less-than comparison operator override.
|
|
52
|
+
|
|
53
|
+
Defaults to True if variable is unassigned.
|
|
54
|
+
"""
|
|
55
|
+
return self.value < x if self.is_assigned else True
|
|
56
|
+
|
|
57
|
+
@override
|
|
58
|
+
def __gt__(self, x:Type) -> bool:
|
|
59
|
+
""" Greater-than comparison operator override.
|
|
60
|
+
|
|
61
|
+
Defaults to True if variable is unassigned.
|
|
62
|
+
"""
|
|
63
|
+
return self.value > x if self.is_assigned else True
|
|
64
|
+
|
|
65
|
+
@override
|
|
66
|
+
def __le__(self, x:Type) -> bool:
|
|
67
|
+
""" Less-than-or-equal comparison operator override. """
|
|
68
|
+
return self.__eq__(x) or self.__lt__(x)
|
|
69
|
+
|
|
70
|
+
@override
|
|
71
|
+
def __ge__(self, x:Type) -> bool:
|
|
72
|
+
""" Greater-than-or-equal comparison operator override. """
|
|
73
|
+
return self.__eq__(x) or self.__gt__(x)
|
|
74
|
+
|
|
75
|
+
@override
|
|
76
|
+
def __add__(self, x:Type) -> Type | "__Variable":
|
|
77
|
+
""" Addition operator override. """
|
|
78
|
+
return self.value + x if self.is_assigned else self
|
|
79
|
+
|
|
80
|
+
@override
|
|
81
|
+
def __sub__(self, x:Type) -> Type | "__Variable":
|
|
82
|
+
""" Subtraction operator override. """
|
|
83
|
+
return self.value - x if self.is_assigned else self
|
|
84
|
+
|
|
85
|
+
@override
|
|
86
|
+
def __mul__(self, x:Type) -> Type | "__Variable":
|
|
87
|
+
""" Multiplication operator override. """
|
|
88
|
+
return self.value * x if self.is_assigned else self
|
|
89
|
+
|
|
90
|
+
@override
|
|
91
|
+
def __truediv__(self, x:Type) -> Type | "__Variable":
|
|
92
|
+
""" True division operator override. """
|
|
93
|
+
return self.value / x if self.is_assigned else self
|
|
94
|
+
|
|
95
|
+
@override
|
|
96
|
+
def __rtruediv__(self, x:Type) -> Type | "__Variable":
|
|
97
|
+
""" Right-hand true division operator override. """
|
|
98
|
+
return x / self.value if self.is_assigned else self
|
|
99
|
+
|
|
100
|
+
@override
|
|
101
|
+
def __mod__(self, x:Type) -> Type | "__Variable":
|
|
102
|
+
""" Modulo operator override. """
|
|
103
|
+
return self.value % x if self.is_assigned else self
|
|
104
|
+
|
|
105
|
+
@override
|
|
106
|
+
def __or__(self, x:Type) -> Type | "__Variable":
|
|
107
|
+
""" Or operator override. """
|
|
108
|
+
return self.value | x if self.is_assigned else self
|
|
109
|
+
|
|
110
|
+
@override
|
|
111
|
+
def __and__(self, x:Type) -> Type | "__Variable":
|
|
112
|
+
""" And operator override. """
|
|
113
|
+
return self.value & x if self.is_assigned else self
|
|
114
|
+
|
|
115
|
+
@override
|
|
116
|
+
def __pow__(self, x:Type) -> Type | "__Variable":
|
|
117
|
+
""" Power operator override. """
|
|
118
|
+
return self.value ** x if self.is_assigned else self
|
|
119
|
+
|
|
120
|
+
@override
|
|
121
|
+
def __neg__(self) -> Type | "__Variable":
|
|
122
|
+
""" Negation operator override. """
|
|
123
|
+
return -self.value if self.is_assigned else self
|
|
124
|
+
|
|
125
|
+
@override
|
|
126
|
+
def __truediv__(self, x:Type) -> Type | "__Variable":
|
|
127
|
+
""" True division operator override. """
|
|
128
|
+
return self.value / x if self.is_assigned else self
|
|
129
|
+
|
|
130
|
+
@override
|
|
131
|
+
def __floordiv__(self, x:Type) -> Type | "__Variable":
|
|
132
|
+
""" Floor division operator override. """
|
|
133
|
+
return self.value // x if self.is_assigned else self
|
|
134
|
+
|
|
135
|
+
@override
|
|
136
|
+
def __abs__(self) -> Type | "__Variable":
|
|
137
|
+
""" Absolute value operator override. """
|
|
138
|
+
return abs(self.value) if self.is_assigned else self
|
|
139
|
+
|
|
140
|
+
class Variable(__Variable, Generic[Type]):
|
|
141
|
+
""" Variable class for Constraint Satisfaction Problems. """
|
|
142
|
+
def __init__(self, domain:Domain[Type], name:str | None = None):
|
|
143
|
+
""" Initialise a variable with a domain and optional name.
|
|
144
|
+
|
|
145
|
+
:param domain: Collection of possible values for the variable.
|
|
146
|
+
:param name: Optional name for the variable.
|
|
147
|
+
"""
|
|
148
|
+
self.__domain = deepcopy(domain) # domain belongs only to one variable
|
|
149
|
+
self.__value:Type | None = None
|
|
150
|
+
self.name = name
|
|
151
|
+
self.__hash = random.random() # unique hash for variable identity
|
|
152
|
+
|
|
153
|
+
@override
|
|
154
|
+
def __hash__(self ) -> int:
|
|
155
|
+
""" Returns a unique hash for the variable. """
|
|
156
|
+
return hash(self.__hash)
|
|
157
|
+
|
|
158
|
+
@property
|
|
159
|
+
def is_assigned(self) -> bool:
|
|
160
|
+
""" Determines whether variable is assigned a value. """
|
|
161
|
+
return self.__value is not None
|
|
162
|
+
|
|
163
|
+
@property
|
|
164
|
+
def value(self) -> Type | None:
|
|
165
|
+
""" Returns the value assigned to the variable """
|
|
166
|
+
return self.__value
|
|
167
|
+
|
|
168
|
+
@value.setter
|
|
169
|
+
def value(self, x: Type | None) -> None:
|
|
170
|
+
""" Set value of variable to x if in domain, else raise ValueError.
|
|
171
|
+
|
|
172
|
+
:param x: Value to assign to variable.
|
|
173
|
+
:raises ValueError: If x is not in variable domain.
|
|
174
|
+
"""
|
|
175
|
+
if x in self.domain or x is None:
|
|
176
|
+
self.__value = x
|
|
177
|
+
else:
|
|
178
|
+
raise ValueError(f"{self.name}: {x} not in domain {self.domain}")
|
|
179
|
+
|
|
180
|
+
@property
|
|
181
|
+
def domain(self) -> Domain[Type]:
|
|
182
|
+
""" Returns the domain of the variable. (read only)"""
|
|
183
|
+
return self.__domain
|
|
184
|
+
|
|
185
|
+
def __repr__(self) -> str:
|
|
186
|
+
""" String representation of the variable. Returns name and either value if assigned or ?. """
|
|
187
|
+
return f"{self.name}({str(self.value) if self.is_assigned else '?'})"
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class Factor(Thing):
|
|
191
|
+
""" Wrapper class for constraints in a CSP. """
|
|
192
|
+
def __init__(self,
|
|
193
|
+
constraint:Callable[..., bool],
|
|
194
|
+
variables:Iterable[Variable] | Variable):
|
|
195
|
+
""" Initialise a Factor with a constraint function and variables.
|
|
196
|
+
|
|
197
|
+
:param constraint: A callable function representing the constraint.
|
|
198
|
+
:param variables: An Variable (or iterable collection of Variable instances) involved in the constraint.
|
|
199
|
+
"""
|
|
200
|
+
assert isinstance(constraint, Callable), "constraint must be callable"
|
|
201
|
+
self.__function = constraint
|
|
202
|
+
if not isinstance(variables, Iterable):
|
|
203
|
+
variables = [variables]
|
|
204
|
+
self.__variables:list[Variable] = aslist(variables)
|
|
205
|
+
if self.__xnary < 1:
|
|
206
|
+
raise ValueError(f"{self}: number of variables must be >1")
|
|
207
|
+
|
|
208
|
+
def __call__(self, *args, **kwargs) -> bool:
|
|
209
|
+
""" Implements ability to call that will evaluate the constraint function. """
|
|
210
|
+
return self.__function(*args, **kwargs)
|
|
211
|
+
|
|
212
|
+
def __iter__(self) -> Iterator[Variable]:
|
|
213
|
+
""" Implements iteration over variables in the factor. """
|
|
214
|
+
return iter(self.__variables)
|
|
215
|
+
|
|
216
|
+
def __contains__(self, variable: Variable) -> bool:
|
|
217
|
+
""" Overrides in keyword behaviour because variable equality
|
|
218
|
+
is checked by value rather than hash
|
|
219
|
+
|
|
220
|
+
:param variable: Variable to check for membership in the factor.
|
|
221
|
+
:return: True if variable is in the factor, else False.
|
|
222
|
+
"""
|
|
223
|
+
return any(variable is _variable for _variable in self)
|
|
224
|
+
|
|
225
|
+
@property
|
|
226
|
+
def is_satisfied(self) -> bool:
|
|
227
|
+
""" Determines whether the constraint is satisfied. """
|
|
228
|
+
if all(v.is_assigned for v in self.__variables):
|
|
229
|
+
return self(*self.__variables)
|
|
230
|
+
elif self.is_global:
|
|
231
|
+
return self.__function(*self.__variables)
|
|
232
|
+
else:
|
|
233
|
+
return True
|
|
234
|
+
|
|
235
|
+
@override
|
|
236
|
+
def __repr__(self) -> str:
|
|
237
|
+
""" String representation of the factor. """
|
|
238
|
+
return str(tuple([str(v.name) for v in self.variables]))
|
|
239
|
+
|
|
240
|
+
@property
|
|
241
|
+
def __xnary(self) -> int:
|
|
242
|
+
""" Determines xnary of the factor. """
|
|
243
|
+
return len(self.variables)
|
|
244
|
+
|
|
245
|
+
@property
|
|
246
|
+
def is_unary(self) -> bool:
|
|
247
|
+
""" Is the factor unary? """
|
|
248
|
+
return self.__xnary == 1
|
|
249
|
+
|
|
250
|
+
@property
|
|
251
|
+
def is_binary(self) -> bool:
|
|
252
|
+
""" Is the factor binary? """
|
|
253
|
+
return self.__xnary == 2
|
|
254
|
+
|
|
255
|
+
@property
|
|
256
|
+
def is_global(self) -> bool:
|
|
257
|
+
""" Is the factor global? """
|
|
258
|
+
return self.__xnary > 2
|
|
259
|
+
|
|
260
|
+
@property
|
|
261
|
+
def arcs(self) -> tuple["Arc", "Arc"]:
|
|
262
|
+
""" Returns the arcs for binary factors.
|
|
263
|
+
|
|
264
|
+
:return: Tuple of arcs (Factor, Variable, Variable), e.g. (self, A, B) and (self, B, A)
|
|
265
|
+
:raises TypeError: If factor is not binary.
|
|
266
|
+
"""
|
|
267
|
+
if not self.is_binary:
|
|
268
|
+
raise TypeError(f"{self}: constraint is not binary")
|
|
269
|
+
L,R = self.variables
|
|
270
|
+
return ((self, L, R), (self, R, L))
|
|
271
|
+
|
|
272
|
+
@property
|
|
273
|
+
def variables(self) -> Collection[Variable]:
|
|
274
|
+
""" Returns the variables involved in the factor. (read only) """
|
|
275
|
+
return self.__variables
|
|
276
|
+
|
|
277
|
+
Arc = tuple[Factor, Variable, Variable] # type alias for arcs in binary factors
|
|
278
|
+
|
|
279
|
+
class Constraint(Factor):
|
|
280
|
+
""" Class alias for Factor to represent constraints in a CSP. """
|
|
281
|
+
pass
|
|
282
|
+
|
|
283
|
+
class ConstraintSatisfactionProblem:
|
|
284
|
+
""" Constraint Satisfaction Problem base class.
|
|
285
|
+
|
|
286
|
+
Defined by variables (and their domains) and constraints.
|
|
287
|
+
|
|
288
|
+
Has attributes
|
|
289
|
+
- variables: Collection of variables in the CSP.
|
|
290
|
+
- domains: Dictionary of variable domains.
|
|
291
|
+
- constraints: Collection of constraints (Factors) in the CSP.
|
|
292
|
+
|
|
293
|
+
Has utility checks including
|
|
294
|
+
- is_consistent: Whether all constraints are satisfied.
|
|
295
|
+
- is_complete: Whether all variables are assigned and all constraints are satisfied.
|
|
296
|
+
- arcs: List of arcs in binary constraints.
|
|
297
|
+
"""
|
|
298
|
+
def __init__(self,
|
|
299
|
+
variables:Collection[Variable],
|
|
300
|
+
constraints:Collection[Factor]) -> None:
|
|
301
|
+
"""
|
|
302
|
+
Constructor for CSP
|
|
303
|
+
|
|
304
|
+
:param variables: Collection of variables in the CSP.
|
|
305
|
+
:param constraints: Collection of constraints (Factors) in the CSP.
|
|
306
|
+
"""
|
|
307
|
+
self.__variables = variables
|
|
308
|
+
self.__constraints = constraints
|
|
309
|
+
|
|
310
|
+
@property
|
|
311
|
+
def variables(self) -> Collection[Variable]:
|
|
312
|
+
""" Read only list of variables in the CSP.
|
|
313
|
+
|
|
314
|
+
Read only.
|
|
315
|
+
|
|
316
|
+
:return: List of variables.
|
|
317
|
+
"""
|
|
318
|
+
return self.__variables
|
|
319
|
+
|
|
320
|
+
@property
|
|
321
|
+
def domains(self) -> dict[Variable, Domain]:
|
|
322
|
+
""" Read only dictionary of variable domains.
|
|
323
|
+
|
|
324
|
+
:return: Dictionary mapping variables to their domains.
|
|
325
|
+
"""
|
|
326
|
+
return {variable: variable.domain for variable in self.variables}
|
|
327
|
+
|
|
328
|
+
@property
|
|
329
|
+
def constraints(self) -> Collection[Factor]:
|
|
330
|
+
""" Read only list of constraints in the CSP."""
|
|
331
|
+
return self.__constraints
|
|
332
|
+
|
|
333
|
+
@property
|
|
334
|
+
def is_consistent(self) -> bool:
|
|
335
|
+
""" Determines whether all constraints are satisfied for current variable assignments. """
|
|
336
|
+
return all(constraint.is_satisfied for constraint in self.constraints)
|
|
337
|
+
|
|
338
|
+
@property
|
|
339
|
+
def is_complete(self) -> bool:
|
|
340
|
+
""" Determines whether all variables are assigned and all constraints are satisfied. """
|
|
341
|
+
return all(variable.is_assigned for variable in self.variables) \
|
|
342
|
+
and self.is_consistent
|
|
343
|
+
|
|
344
|
+
@property
|
|
345
|
+
def arcs(self) -> list[Arc]:
|
|
346
|
+
""" Returns list of arcs in binary constraints. """
|
|
347
|
+
return [arc for constraint in self.constraints if constraint.is_binary
|
|
348
|
+
for arc in constraint.arcs]
|
|
349
|
+
|
|
350
|
+
def __repr__(self) -> str:
|
|
351
|
+
""" String representation of the CSP. """
|
|
352
|
+
return str({v.name: v for v in self.variables})
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
CSP = ConstraintSatisfactionProblem # type alias
|
|
356
|
+
|
|
357
|
+
class CSPAgent(Agent):
|
|
358
|
+
""" ConstraintSatisfaction Problem Agent base class. """
|
|
359
|
+
def __repr__(self) -> str:
|
|
360
|
+
return "🤖"
|
|
361
|
+
|
|
362
|
+
@override
|
|
363
|
+
def program(self, percept:CSP) -> tuple[Literal["solve"], CSP]:
|
|
364
|
+
""" Constraint Satisfaction Problem agent program. """
|
|
365
|
+
return ("solve", percept)
|
|
366
|
+
|
|
367
|
+
def solve(self, csp: CSP) -> CSP:
|
|
368
|
+
""" Actuator for solving CSP problems """
|
|
369
|
+
raise NotImplementedError("Need to implement solve method in subclass")
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
## Environment ##
|
|
373
|
+
class CSPRunnerEnvironment(Environment):
|
|
374
|
+
""" Environment for running CSP agents. """
|
|
375
|
+
def __init__(self, csp:CSP, solution=None, *args, **kwargs):
|
|
376
|
+
super().__init__(*args, **kwargs)
|
|
377
|
+
self.csp = csp
|
|
378
|
+
self.solution = solution
|
|
379
|
+
self.__done = False
|
|
380
|
+
|
|
381
|
+
@property
|
|
382
|
+
def is_done(self) -> bool:
|
|
383
|
+
""" Check if CSP is solved. """
|
|
384
|
+
return self.__done
|
|
385
|
+
|
|
386
|
+
@override
|
|
387
|
+
def percept(self, agent:CSPAgent) -> CSP:
|
|
388
|
+
""" Provides agent with the CSP as percept. """
|
|
389
|
+
return self.csp
|
|
390
|
+
|
|
391
|
+
@override
|
|
392
|
+
def execute_action(self,
|
|
393
|
+
agent:CSPAgent,
|
|
394
|
+
action:tuple[Literal["solve"], CSP]):
|
|
395
|
+
""" Execute action for CSP agent.
|
|
396
|
+
|
|
397
|
+
:param agent: The CSP agent executing the action.
|
|
398
|
+
:param action: The action to execute, expected to be a tuple ("solve", csp).
|
|
399
|
+
"""
|
|
400
|
+
command, csp = action
|
|
401
|
+
if command == "solve":
|
|
402
|
+
solution = agent.solve(csp)
|
|
403
|
+
else:
|
|
404
|
+
raise ValueError(f"{agent}: unknown command {command}")
|
|
405
|
+
|
|
406
|
+
if solution:
|
|
407
|
+
print(f"final solution:\n{solution}")
|
|
408
|
+
if self.solution is not None:
|
|
409
|
+
print(f"correct solution:\n{self.solution}")
|
|
410
|
+
else:
|
|
411
|
+
print("no solution found")
|
|
412
|
+
self.__done = True
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
@override
|
|
416
|
+
def run(self, steps:int=1, pause_for_user:bool=False) -> None:
|
|
417
|
+
""" Run the CSP environment for a number of steps."""
|
|
418
|
+
if pause_for_user:
|
|
419
|
+
input("Press enter to start simulation")
|
|
420
|
+
|
|
421
|
+
print(f"{self}: Running for {steps} iterations.")
|
|
422
|
+
for i in range(steps):
|
|
423
|
+
if self.is_done:
|
|
424
|
+
if steps:
|
|
425
|
+
print(f"{self}: Simulation complete after {i} of {steps} iterations.")
|
|
426
|
+
return
|
|
427
|
+
self.step()
|
|
428
|
+
if steps:
|
|
429
|
+
print(f"{self}: Simulation complete after {steps} of {steps} iterations.")
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
from collections.abc import Iterable
|
|
2
|
+
from collections import deque
|
|
3
|
+
from copy import deepcopy
|
|
4
|
+
|
|
5
|
+
from typing import TypeVar
|
|
6
|
+
|
|
7
|
+
# from ..csp import Variable, CSP, Factor # avoid circular imports
|
|
8
|
+
Variable = TypeVar("Variable")
|
|
9
|
+
CSP = TypeVar("CSP")
|
|
10
|
+
Factor = TypeVar("Factor")
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
|
|
14
|
+
def alldiff(*variables:tuple[Variable|Iterable[Variable]]) -> bool:
|
|
15
|
+
""" All different constraint function"""
|
|
16
|
+
if len(variables) == 1: # unwrap if given as a single iterable
|
|
17
|
+
if not isinstance(variables[0], Iterable):
|
|
18
|
+
return True
|
|
19
|
+
variables = variables[0]
|
|
20
|
+
# variables = [
|
|
21
|
+
# variable.value for variable in variable if variable.is_assigned]
|
|
22
|
+
values = [
|
|
23
|
+
variable.value for variable in variables
|
|
24
|
+
if variable.is_assigned]
|
|
25
|
+
return len(set(values)) == len(values) # all different
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def aslist(npcol:Iterable) -> list:
|
|
29
|
+
""" Enforces list type """
|
|
30
|
+
return np.array(npcol).ravel().tolist()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def revise(factor:Factor, A:Variable, B:Variable) -> bool:
|
|
34
|
+
""" Revise method for AC-3 algorithm.
|
|
35
|
+
|
|
36
|
+
Checks every value in the domain of variable A to see if there is
|
|
37
|
+
a corresponding value in the domain of variable B that satisfies. Removes any that do not.
|
|
38
|
+
|
|
39
|
+
:param factor: The binary factor between variables A and B
|
|
40
|
+
:param A: The first variable in the binary factor
|
|
41
|
+
:param B: The other variable in the binary factor
|
|
42
|
+
"""
|
|
43
|
+
if A.is_assigned: return False # nothing to revise
|
|
44
|
+
|
|
45
|
+
is_revised = False
|
|
46
|
+
|
|
47
|
+
domain = deepcopy(A.domain) # copy to avoid set size change errors
|
|
48
|
+
for value in domain:
|
|
49
|
+
A.value = value # temporarily assign A
|
|
50
|
+
is_valid_B = False
|
|
51
|
+
for _value in B.domain:
|
|
52
|
+
B.value = _value # temporarily assign B
|
|
53
|
+
if factor.is_satisfied: is_valid_B = True
|
|
54
|
+
B.value = None # reset B
|
|
55
|
+
if not is_valid_B: # no valid B found for A=value
|
|
56
|
+
A.domain.remove(value) # remove value from A's domain
|
|
57
|
+
is_revised = True
|
|
58
|
+
A.value = None # reset A
|
|
59
|
+
return is_revised
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def ac3(csp:CSP, log:bool=False, inplace:bool=True) -> CSP | bool | None:
|
|
63
|
+
""" AC-3 algorithm for enforcing arc consistency on a CSP.
|
|
64
|
+
|
|
65
|
+
:param csp: The constraint satisfaction problem to enforce arc consistency on
|
|
66
|
+
:param log: If True, prints log messages during processing
|
|
67
|
+
:param inplace: If True, modifies the given CSP, otherwise returns a new CSP
|
|
68
|
+
:return: The modified CSP if inplace is False, True if inplace is True and successful
|
|
69
|
+
"""
|
|
70
|
+
if not inplace: csp = deepcopy(csp) # work on a copy if not inplace
|
|
71
|
+
|
|
72
|
+
arcs = deque(csp.arcs) # queue of arcs to consider
|
|
73
|
+
|
|
74
|
+
while len(arcs) > 0: # while there are arcs to consider
|
|
75
|
+
f, A, B = arcs.popleft() # end of queue
|
|
76
|
+
if log:
|
|
77
|
+
print(f"considering arc from {A.name} to {B.name}")
|
|
78
|
+
print(f" before: {A.name} in {A.domain}, {B.name} in {B.domain}")
|
|
79
|
+
if revise(f, A, B): # if we revised A's domain
|
|
80
|
+
if log:
|
|
81
|
+
print(
|
|
82
|
+
f" after: {A.name} in {A.domain}, {B.name} in {B.domain}")
|
|
83
|
+
if len(A.domain) == 0: # domain wiped out, failure
|
|
84
|
+
return False if inplace else None # no possible solution
|
|
85
|
+
|
|
86
|
+
for constraint in csp.constraints: # update related arcs
|
|
87
|
+
if log:
|
|
88
|
+
print(f" do we need to check constraint {constraint}")
|
|
89
|
+
print(f" is binary? {constraint.is_binary}")
|
|
90
|
+
print(f" contains {A.name}? {A in constraint}")
|
|
91
|
+
print(f" doesn't contain {B.name}? {B not in constraint}")
|
|
92
|
+
if constraint.is_binary \
|
|
93
|
+
and A in constraint\
|
|
94
|
+
and B not in constraint:
|
|
95
|
+
for arc in constraint.arcs:
|
|
96
|
+
if arc[-1] is A: # arc points to A
|
|
97
|
+
if arc not in arcs: # not already in queue
|
|
98
|
+
if log:
|
|
99
|
+
print(f" adding {arc} to arcs frontier")
|
|
100
|
+
arcs.append(arc) # add new arc to consider
|
|
101
|
+
elif log:
|
|
102
|
+
print(f" arc {arc} already in frontier")
|
|
103
|
+
elif log:
|
|
104
|
+
print(f" after: {A.name} in {A.domain}, {B.name} in {B.domain} (no change)")
|
|
105
|
+
return True if inplace else csp
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def make_node_consistent(csp, inplace=True) -> CSP | None:
|
|
109
|
+
""" Makes the CSP node consistent by enforcing unary constraints.
|
|
110
|
+
|
|
111
|
+
:param csp: The constraint satisfaction problem to make node consistent
|
|
112
|
+
:param inplace: If True, modifies the given CSP, otherwise returns a new CSP
|
|
113
|
+
:return: The modified CSP if inplace is False, otherwise None
|
|
114
|
+
"""
|
|
115
|
+
if not inplace: csp = deepcopy(csp)
|
|
116
|
+
|
|
117
|
+
for variable in csp.variables:
|
|
118
|
+
if variable.is_assigned: continue # ignore any assigned variables
|
|
119
|
+
domain = variable.domain.copy() # copy this to avoid set size change errors
|
|
120
|
+
for value in domain:
|
|
121
|
+
variable.value = value
|
|
122
|
+
for constraint in csp.constraints:
|
|
123
|
+
if constraint.is_unary and variable in constraint:
|
|
124
|
+
if not constraint.is_satisfied:
|
|
125
|
+
variable.domain.remove(value)
|
|
126
|
+
break
|
|
127
|
+
variable.value = None
|
|
128
|
+
if not inplace: return csp
|