co2114 2026.1.1__py3-none-any.whl → 2026.1.3__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.
@@ -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 terination message and exit
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
@@ -1,194 +1,230 @@
1
1
  import numpy as np
2
2
  import random
3
3
  from ...agent.things import Thing, Agent
4
+ from ...agent.environment import Environment
4
5
  from collections.abc import Callable, Iterable
5
6
  from copy import deepcopy
6
7
 
7
- from .util import alldiff
8
+ from typing import Literal, override, TypeVar, Collection, Generic, Iterator
9
+
10
+ from .util import aslist
8
11
 
9
12
  __all__ = [
10
13
  'util',
11
14
  'CSPAgent',
12
15
  'ConstraintSatisfactionProblem',
16
+ 'CSPRunnerEnvironment',
13
17
  'Variable',
14
18
  'Factor']
15
19
 
16
- class CSPAgent(Agent):
17
- def __repr__(self):
18
- return "🤖"
19
-
20
- def program(self, percept):
21
- pass
22
-
23
- def solve(self):
24
- raise NotImplementedError
25
-
26
-
27
- class ConstraintSatisfactionProblem:
28
- def __init__(self, variables, constraints):
29
- self.__variables = variables
30
- self.__constraints = constraints
31
-
32
- @property
33
- def variables(self):
34
- """ List of variables in the CSP.
35
-
36
- :return: List of variables.
37
- """
38
- return self.__variables
39
-
40
- @property
41
- def domains(self):
42
- return {variable: variable.domain for variable in self.variables}
20
+ # === CLASSES === #
21
+ Type = TypeVar("Type")
22
+ Domain = Collection[Type]
43
23
 
44
- @property
45
- def constraints(self):
46
- return self.__constraints
47
-
48
- @property
49
- def is_consistent(self):
50
- return all(constraint.is_satisfied for constraint in self.constraints)
24
+ class __Variable(Thing, Generic[Type]):
25
+ """ Abstract Base Class for Variables in a CSP.
51
26
 
27
+ Defines operations on variables to allow arithmetic and comparison
28
+ """
52
29
  @property
53
- def is_complete(self):
54
- return all(variable.is_assigned for variable in self.variables) \
55
- and self.is_consistent
56
-
57
- @property
58
- def arcs(self):
59
- return [arc for constraint in self.constraints if constraint.is_binary
60
- for arc in constraint.arcs]
61
-
62
- def __repr__(self):
63
- return str({v.name: v for v in self.variables})
64
-
65
-
66
- class __Variable(Thing):
67
- @property
68
- def value(self):
30
+ def value(self) -> Type:
31
+ """ The value assigned to the variable (read only) """
69
32
  return self.__value
70
33
 
71
34
  @property
72
- def is_assigned(self):
35
+ def is_assigned(self) -> bool:
36
+ """ Whether the variable has been assigned a value. """
73
37
  return hasattr(self, '__value') and self.__value is not None
74
38
 
75
- def __eq__(self, x):
39
+ @override
40
+ def __eq__(self, x:Type) -> bool:
41
+ """ Equality comparison operator override. """
76
42
  return (x == self.value)
77
43
 
78
- def __ne__(self, x):
44
+ @override
45
+ def __ne__(self, x:Type) -> bool:
46
+ """ Inequality comparison operator override. """
79
47
  return (x != self.value)
80
48
 
81
- def __lt__(self, x):
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
+ """
82
55
  return self.value < x if self.is_assigned else True
83
56
 
84
- def __gt__(self, x):
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
+ """
85
63
  return self.value > x if self.is_assigned else True
86
64
 
87
- def __le__(self, x):
65
+ @override
66
+ def __le__(self, x:Type) -> bool:
67
+ """ Less-than-or-equal comparison operator override. """
88
68
  return self.__eq__(x) or self.__lt__(x)
89
69
 
90
- def __ge__(self, x):
70
+ @override
71
+ def __ge__(self, x:Type) -> bool:
72
+ """ Greater-than-or-equal comparison operator override. """
91
73
  return self.__eq__(x) or self.__gt__(x)
92
74
 
93
- def __add__(self, x):
75
+ @override
76
+ def __add__(self, x:Type) -> Type | "__Variable":
77
+ """ Addition operator override. """
94
78
  return self.value + x if self.is_assigned else self
95
79
 
96
- def __sub__(self, x):
80
+ @override
81
+ def __sub__(self, x:Type) -> Type | "__Variable":
82
+ """ Subtraction operator override. """
97
83
  return self.value - x if self.is_assigned else self
98
84
 
99
- def __mul__(self, x):
85
+ @override
86
+ def __mul__(self, x:Type) -> Type | "__Variable":
87
+ """ Multiplication operator override. """
100
88
  return self.value * x if self.is_assigned else self
101
89
 
102
- def __truediv__(self, x):
90
+ @override
91
+ def __truediv__(self, x:Type) -> Type | "__Variable":
92
+ """ True division operator override. """
103
93
  return self.value / x if self.is_assigned else self
104
94
 
105
- def __rtruediv__(self, x):
95
+ @override
96
+ def __rtruediv__(self, x:Type) -> Type | "__Variable":
97
+ """ Right-hand true division operator override. """
106
98
  return x / self.value if self.is_assigned else self
107
99
 
108
- def __mod__(self, x):
100
+ @override
101
+ def __mod__(self, x:Type) -> Type | "__Variable":
102
+ """ Modulo operator override. """
109
103
  return self.value % x if self.is_assigned else self
110
104
 
111
- def __or__(self, x):
105
+ @override
106
+ def __or__(self, x:Type) -> Type | "__Variable":
107
+ """ Or operator override. """
112
108
  return self.value | x if self.is_assigned else self
113
109
 
114
- def __and__(self, x):
110
+ @override
111
+ def __and__(self, x:Type) -> Type | "__Variable":
112
+ """ And operator override. """
115
113
  return self.value & x if self.is_assigned else self
116
114
 
117
- def __pow__(self, x):
115
+ @override
116
+ def __pow__(self, x:Type) -> Type | "__Variable":
117
+ """ Power operator override. """
118
118
  return self.value ** x if self.is_assigned else self
119
119
 
120
- def __neg__(self):
120
+ @override
121
+ def __neg__(self) -> Type | "__Variable":
122
+ """ Negation operator override. """
121
123
  return -self.value if self.is_assigned else self
122
124
 
123
- def __truediv__(self, x):
124
- return self.value/x if self.is_assigned else self
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
125
129
 
126
- def __floordiv__(self, x):
127
- return self.value//x if self.is_assigned else self
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
128
134
 
129
- def __abs__(self):
135
+ @override
136
+ def __abs__(self) -> Type | "__Variable":
137
+ """ Absolute value operator override. """
130
138
  return abs(self.value) if self.is_assigned else self
131
139
 
132
-
133
- class Variable(__Variable):
134
- def __init__(self, domain, name=None):
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
+ """
135
148
  self.__domain = deepcopy(domain) # domain belongs only to one variable
136
- self.__value = None
149
+ self.__value:Type | None = None
137
150
  self.name = name
138
- self.__hash = random.random()
151
+ self.__hash = random.random() # unique hash for variable identity
139
152
 
140
- def __hash__(self):
153
+ @override
154
+ def __hash__(self ) -> int:
155
+ """ Returns a unique hash for the variable. """
141
156
  return hash(self.__hash)
142
157
 
143
158
  @property
144
- def is_assigned(self):
159
+ def is_assigned(self) -> bool:
160
+ """ Determines whether variable is assigned a value. """
145
161
  return self.__value is not None
146
162
 
147
163
  @property
148
- def value(self):
164
+ def value(self) -> Type | None:
165
+ """ Returns the value assigned to the variable """
149
166
  return self.__value
150
167
 
151
168
  @value.setter
152
- def value(self, x):
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
+ """
153
175
  if x in self.domain or x is None:
154
176
  self.__value = x
155
177
  else:
156
178
  raise ValueError(f"{self.name}: {x} not in domain {self.domain}")
157
179
 
158
180
  @property
159
- def domain(self):
181
+ def domain(self) -> Domain[Type]:
182
+ """ Returns the domain of the variable. (read only)"""
160
183
  return self.__domain
161
184
 
162
- def __repr__(self):
185
+ def __repr__(self) -> str:
186
+ """ String representation of the variable. Returns name and either value if assigned or ?. """
163
187
  return f"{self.name}({str(self.value) if self.is_assigned else '?'})"
164
188
 
165
189
 
166
190
  class Factor(Thing):
167
- def __init__(self, constraint, variables):
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
+ """
168
200
  assert isinstance(constraint, Callable), "constraint must be callable"
169
201
  self.__function = constraint
170
202
  if not isinstance(variables, Iterable):
171
203
  variables = [variables]
172
- self.__variables = np.array(variables).ravel().tolist()
204
+ self.__variables:list[Variable] = aslist(variables)
173
205
  if self.__xnary < 1:
174
206
  raise ValueError(f"{self}: number of variables must be >1")
175
207
 
176
- def __call__(self, *args, **kwargs):
208
+ def __call__(self, *args, **kwargs) -> bool:
209
+ """ Implements ability to call that will evaluate the constraint function. """
177
210
  return self.__function(*args, **kwargs)
178
211
 
179
- def __iter__(self):
212
+ def __iter__(self) -> Iterator[Variable]:
213
+ """ Implements iteration over variables in the factor. """
180
214
  return iter(self.__variables)
181
215
 
182
- def __contains__(self, variable):
183
- """
184
- overrides in keyword behaviour because variable equality
185
- is checked by value rather than hash
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.
186
222
  """
187
223
  return any(variable is _variable for _variable in self)
188
224
 
189
-
190
225
  @property
191
- def is_satisfied(self):
226
+ def is_satisfied(self) -> bool:
227
+ """ Determines whether the constraint is satisfied. """
192
228
  if all(v.is_assigned for v in self.__variables):
193
229
  return self(*self.__variables)
194
230
  elif self.is_global:
@@ -196,67 +232,198 @@ class Factor(Thing):
196
232
  else:
197
233
  return True
198
234
 
199
- def __repr__(self):
235
+ @override
236
+ def __repr__(self) -> str:
237
+ """ String representation of the factor. """
200
238
  return str(tuple([str(v.name) for v in self.variables]))
201
239
 
202
240
  @property
203
- def __xnary(self):
241
+ def __xnary(self) -> int:
242
+ """ Determines xnary of the factor. """
204
243
  return len(self.variables)
205
244
 
206
245
  @property
207
- def is_unary(self):
246
+ def is_unary(self) -> bool:
247
+ """ Is the factor unary? """
208
248
  return self.__xnary == 1
209
249
 
210
250
  @property
211
- def is_binary(self):
251
+ def is_binary(self) -> bool:
252
+ """ Is the factor binary? """
212
253
  return self.__xnary == 2
213
254
 
214
255
  @property
215
- def is_global(self):
256
+ def is_global(self) -> bool:
257
+ """ Is the factor global? """
216
258
  return self.__xnary > 2
217
259
 
218
260
  @property
219
- def arcs(self):
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
+ """
220
267
  if not self.is_binary:
221
268
  raise TypeError(f"{self}: constraint is not binary")
222
269
  L,R = self.variables
223
- return [(self, L, R), (self, R, L)]
270
+ return ((self, L, R), (self, R, L))
224
271
 
225
272
  @property
226
- def variables(self):
273
+ def variables(self) -> Collection[Variable]:
274
+ """ Returns the variables involved in the factor. (read only) """
227
275
  return self.__variables
228
276
 
277
+ Arc = tuple[Factor, Variable, Variable] # type alias for arcs in binary factors
229
278
 
230
279
  class Constraint(Factor):
280
+ """ Class alias for Factor to represent constraints in a CSP. """
231
281
  pass
232
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
233
309
 
234
- ## NOT CURRENTLY VIABLE DO NOT USE
235
- # from agent.environment import Environment
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}
236
327
 
237
- # class __CSPEnvironment(Environment):
238
- # def __init__(self, problem, *args, **kwargs):
239
- # super().__init__(*args, **kwargs)
240
- # self.__csp = problem
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
+
241
354
 
242
- # @property
243
- # def csp(self):
244
- # return self.__csp
355
+ CSP = ConstraintSatisfactionProblem # type alias
245
356
 
246
- # @property
247
- # def variables(self):
248
- # return self.csp.variables
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)
249
366
 
250
- # @property
251
- # def constraints(self):
252
- # return self.csp.constraints
367
+ def solve(self, csp: CSP) -> CSP:
368
+ """ Actuator for solving CSP problems """
369
+ raise NotImplementedError("Need to implement solve method in subclass")
253
370
 
254
- # @property
255
- # def domains(self):
256
- # return self.csp.domains
257
371
 
258
- # @property
259
- # def is_done(self):
260
- # return all(variable.is_assigned for variable in self.variables) and \
261
- # all(constraint.is_satisfied for constraint in self.constraints)
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
262
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.")