co2114 2026.1.2__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.
Files changed (41) hide show
  1. {co2114-2026.1.2 → co2114-2026.1.3}/PKG-INFO +1 -1
  2. co2114-2026.1.3/co2114/constraints/csp/__init__.py +429 -0
  3. co2114-2026.1.3/co2114/constraints/csp/util.py +128 -0
  4. co2114-2026.1.3/co2114/constraints/sudoku.py +191 -0
  5. co2114-2026.1.3/co2114/optimisation/__init__.py +1 -0
  6. co2114-2026.1.3/co2114/optimisation/adversarial.py +387 -0
  7. {co2114-2026.1.2 → co2114-2026.1.3}/co2114.egg-info/PKG-INFO +1 -1
  8. {co2114-2026.1.2 → co2114-2026.1.3}/co2114.egg-info/SOURCES.txt +1 -1
  9. {co2114-2026.1.2 → co2114-2026.1.3}/setup.py +1 -1
  10. co2114-2026.1.2/co2114/constraints/csp/__init__.py +0 -262
  11. co2114-2026.1.2/co2114/constraints/csp/util.py +0 -89
  12. co2114-2026.1.2/co2114/constraints/sudoku.py +0 -113
  13. co2114-2026.1.2/co2114/optimisation/__init__.py +0 -1
  14. co2114-2026.1.2/co2114/optimisation/minimax.py +0 -185
  15. {co2114-2026.1.2 → co2114-2026.1.3}/LICENSE +0 -0
  16. {co2114-2026.1.2 → co2114-2026.1.3}/README.md +0 -0
  17. {co2114-2026.1.2 → co2114-2026.1.3}/co2114/__init__.py +0 -0
  18. {co2114-2026.1.2 → co2114-2026.1.3}/co2114/agent/__init__.py +0 -0
  19. {co2114-2026.1.2 → co2114-2026.1.3}/co2114/agent/environment.py +0 -0
  20. {co2114-2026.1.2 → co2114-2026.1.3}/co2114/agent/things.py +0 -0
  21. {co2114-2026.1.2 → co2114-2026.1.3}/co2114/constraints/__init__.py +0 -0
  22. {co2114-2026.1.2 → co2114-2026.1.3}/co2114/constraints/magic.py +0 -0
  23. {co2114-2026.1.2 → co2114-2026.1.3}/co2114/engine.py +0 -0
  24. {co2114-2026.1.2 → co2114-2026.1.3}/co2114/optimisation/planning.py +0 -0
  25. {co2114-2026.1.2 → co2114-2026.1.3}/co2114/optimisation/things.py +0 -0
  26. {co2114-2026.1.2 → co2114-2026.1.3}/co2114/reasoning/__init__.py +0 -0
  27. {co2114-2026.1.2 → co2114-2026.1.3}/co2114/reasoning/cluedo.py +0 -0
  28. {co2114-2026.1.2 → co2114-2026.1.3}/co2114/reasoning/inference.py +0 -0
  29. {co2114-2026.1.2 → co2114-2026.1.3}/co2114/reasoning/logic.py +0 -0
  30. {co2114-2026.1.2 → co2114-2026.1.3}/co2114/search/__init__.py +0 -0
  31. {co2114-2026.1.2 → co2114-2026.1.3}/co2114/search/graph.py +0 -0
  32. {co2114-2026.1.2 → co2114-2026.1.3}/co2114/search/maze.py +0 -0
  33. {co2114-2026.1.2 → co2114-2026.1.3}/co2114/search/things.py +0 -0
  34. {co2114-2026.1.2 → co2114-2026.1.3}/co2114/search/util.py +0 -0
  35. {co2114-2026.1.2 → co2114-2026.1.3}/co2114/util/__init__.py +0 -0
  36. {co2114-2026.1.2 → co2114-2026.1.3}/co2114/util/colours.py +0 -0
  37. {co2114-2026.1.2 → co2114-2026.1.3}/co2114/util/fonts.py +0 -0
  38. {co2114-2026.1.2 → co2114-2026.1.3}/co2114.egg-info/dependency_links.txt +0 -0
  39. {co2114-2026.1.2 → co2114-2026.1.3}/co2114.egg-info/requires.txt +0 -0
  40. {co2114-2026.1.2 → co2114-2026.1.3}/co2114.egg-info/top_level.txt +0 -0
  41. {co2114-2026.1.2 → co2114-2026.1.3}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: co2114
3
- Version: 2026.1.2
3
+ Version: 2026.1.3
4
4
  Summary: codebase for co2114
5
5
  Author: wil ward
6
6
  Requires-Python: >=3.12
@@ -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