co2114 2025.2.2__py3-none-any.whl → 2026.0.2__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.
- co2114/agent/environment.py +300 -134
- co2114/agent/things.py +34 -10
- co2114/engine.py +114 -37
- co2114/search/graph.py +278 -129
- co2114/search/things.py +38 -14
- co2114/util/__init__.py +35 -11
- {co2114-2025.2.2.dist-info → co2114-2026.0.2.dist-info}/METADATA +4 -3
- {co2114-2025.2.2.dist-info → co2114-2026.0.2.dist-info}/RECORD +11 -11
- {co2114-2025.2.2.dist-info → co2114-2026.0.2.dist-info}/WHEEL +1 -1
- {co2114-2025.2.2.dist-info → co2114-2026.0.2.dist-info/licenses}/LICENSE +0 -0
- {co2114-2025.2.2.dist-info → co2114-2026.0.2.dist-info}/top_level.txt +0 -0
co2114/search/graph.py
CHANGED
|
@@ -1,13 +1,24 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
import math
|
|
2
4
|
import warnings
|
|
3
5
|
import json
|
|
4
6
|
|
|
7
|
+
from typing import Iterable, Iterator, override, Callable, Collection, TypeVar
|
|
8
|
+
|
|
5
9
|
from matplotlib import pyplot as plt
|
|
10
|
+
from matplotlib.figure import Figure # for type hinting
|
|
6
11
|
|
|
7
12
|
from ..agent.environment import Environment
|
|
8
|
-
from .things import Thing, Agent, UtilityBasedAgent
|
|
13
|
+
from .things import Thing, Agent, UtilityBasedAgent, Action
|
|
14
|
+
|
|
15
|
+
Numeric = int | float | None
|
|
16
|
+
Location = Thing | Iterable
|
|
17
|
+
Label = str
|
|
18
|
+
# Weight = Numeric
|
|
9
19
|
|
|
10
|
-
|
|
20
|
+
# Example graph dictionary representing UK cities and distances between them
|
|
21
|
+
UK_CITIES_GRAPH:dict[str, Iterable[str | tuple[str,str] | int]] = {
|
|
11
22
|
"nodes": ["Edinburgh", "Glasgow", "Manchester", "Liverpool",
|
|
12
23
|
"Newcastle", "York", "Sheffield", "Leicester",
|
|
13
24
|
"London", "Bath", "Bristol", "Exeter", "Cardiff", "Birmingham"],
|
|
@@ -27,174 +38,276 @@ UK_CITIES_GRAPH = {
|
|
|
27
38
|
|
|
28
39
|
|
|
29
40
|
class Node(Thing):
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
self.
|
|
34
|
-
|
|
41
|
+
""" A node in a graph structure."""
|
|
42
|
+
def __init__(self, label:Label = "") -> None:
|
|
43
|
+
""" Create a node with given label. """
|
|
44
|
+
self.label:Label = label # identifier for node
|
|
45
|
+
self.neighbours:set[Node] = set() # set of neighbouring nodes
|
|
46
|
+
self.weights:dict[Node, Numeric] = {} # mapping of neighbouring nodes to edge weights
|
|
47
|
+
self.location:Location | None = None # optional location attribute
|
|
48
|
+
|
|
49
|
+
@override
|
|
50
|
+
def __repr__(self) -> str:
|
|
51
|
+
""" String representation of node, returning its label. """
|
|
35
52
|
return self.label
|
|
36
|
-
|
|
37
|
-
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def add_neighbour(self, node:Node, weight:Numeric = None) -> None:
|
|
56
|
+
""" Add a neighbouring node with optional edge weight.
|
|
57
|
+
|
|
58
|
+
:param node: Node to be added as a neighbour.
|
|
59
|
+
:param weight: Weight of edge to neighbour (default None).
|
|
60
|
+
"""
|
|
61
|
+
if not isinstance(node, Node): # type check
|
|
38
62
|
raise TypeError(f"{self}: {node} is not a Node")
|
|
39
|
-
|
|
63
|
+
|
|
64
|
+
if node not in self.neighbours: # avoid duplicate edges
|
|
40
65
|
self.neighbours.add(node)
|
|
41
66
|
self.weights[node] = weight
|
|
42
|
-
node.add_neighbour(self, weight)
|
|
67
|
+
node.add_neighbour(self, weight) # undirected graph
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
Edge = tuple[Node, Node, Numeric] # edge type alias
|
|
72
|
+
|
|
43
73
|
|
|
44
74
|
class Graph:
|
|
45
|
-
|
|
46
|
-
|
|
75
|
+
""" A graph structure comprising nodes and edges. """
|
|
76
|
+
def __init__(self) -> None:
|
|
77
|
+
""" Create an empty graph. """
|
|
78
|
+
self.nodes:set[Node] = set()
|
|
47
79
|
|
|
48
|
-
|
|
80
|
+
|
|
81
|
+
def __iter__(self) -> Iterator[Node]:
|
|
82
|
+
""" Iterate over nodes in the graph. """
|
|
49
83
|
return iter(self.nodes)
|
|
50
84
|
|
|
51
|
-
|
|
52
|
-
|
|
85
|
+
|
|
86
|
+
def add_node(self, node:Node) -> None:
|
|
87
|
+
""" Add a node and its neighbours to the graph.
|
|
88
|
+
|
|
89
|
+
:param node: Node to be added to the graph.
|
|
90
|
+
"""
|
|
91
|
+
if not isinstance(node, Node): # type check
|
|
53
92
|
raise TypeError(f"{self}: {node} is not a Node")
|
|
54
|
-
|
|
93
|
+
|
|
94
|
+
if node not in self.nodes: # avoid duplicate nodes
|
|
55
95
|
self.nodes.add(node)
|
|
56
|
-
for neighbour in node.neighbours:
|
|
96
|
+
for neighbour in node.neighbours: # add neighbours recursively
|
|
57
97
|
self.add_node(neighbour)
|
|
58
98
|
|
|
59
|
-
|
|
99
|
+
|
|
100
|
+
def plot_nodes(
|
|
101
|
+
self,
|
|
102
|
+
condition:Callable | None = None,
|
|
103
|
+
init:Node | Label | None = None,
|
|
104
|
+
labels:Iterable[Label] | None = None) -> Figure:
|
|
105
|
+
""" Plot the nodes in the graph
|
|
106
|
+
|
|
107
|
+
:param condition: Function to filter nodes to be plotted. If None, all nodes are plotted.
|
|
108
|
+
:param init: Node to use as the root of the plot. If None, a random node is used.
|
|
109
|
+
:param labels: Iterable of labels for the nodes. If None, node labels are used.
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
# filter nodes based on condition
|
|
60
113
|
nodes = self.nodes if condition is None else \
|
|
61
114
|
{node for node in self.nodes if condition(node)}
|
|
115
|
+
|
|
116
|
+
# handle empty graph
|
|
62
117
|
if len(nodes) == 0:
|
|
63
118
|
return plt.figure(figsize=(8,8))
|
|
119
|
+
|
|
120
|
+
# determine initial node for layout
|
|
121
|
+
_init = Node() # placeholder
|
|
64
122
|
if not isinstance(init, Node):
|
|
65
123
|
if init is None:
|
|
66
|
-
|
|
67
|
-
|
|
124
|
+
_init = next(iter(nodes)) # random node
|
|
125
|
+
elif isinstance(init, Label): # label provided
|
|
68
126
|
_flag = True
|
|
69
|
-
for node in nodes:
|
|
127
|
+
for node in nodes: # find node with matching label
|
|
70
128
|
if init == node.label:
|
|
71
|
-
|
|
129
|
+
_init = node
|
|
72
130
|
_flag = False
|
|
73
131
|
break
|
|
74
132
|
if _flag:
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
133
|
+
_init = next(iter(nodes))
|
|
134
|
+
else:
|
|
135
|
+
raise TypeError(f"{self}: {init} is not a Node or label")
|
|
136
|
+
elif init not in nodes: # node not in filtered set
|
|
137
|
+
_init = next(iter(nodes)) # random node
|
|
78
138
|
|
|
79
|
-
locs = {
|
|
80
|
-
tree = {0: {
|
|
139
|
+
locs = {_init: (0.,0.)} # locations of nodes
|
|
140
|
+
tree = {0: {_init}} # nodes at each distance from init
|
|
81
141
|
dist = 1 # dist from init
|
|
82
|
-
tree[dist] =
|
|
142
|
+
tree[dist] = _init.neighbours & nodes # set intersection
|
|
83
143
|
for i, node in enumerate(tree[dist]):
|
|
84
|
-
locs[node] = (dist, i-0.5*len(tree[dist]))
|
|
144
|
+
locs[node] = (dist, i-0.5*len(tree[dist])) # initial layout
|
|
85
145
|
|
|
86
|
-
while len(tree[dist]) > 0:
|
|
146
|
+
while len(tree[dist]) > 0: # expand tree
|
|
87
147
|
tree[dist+1] = set()
|
|
148
|
+
# find new nodes at dist+1 from init
|
|
88
149
|
for node in tree[dist]:
|
|
89
150
|
neighbours = node.neighbours & nodes # set intersection
|
|
90
151
|
for _node in neighbours:
|
|
91
152
|
if _node not in locs and _node not in tree[dist+1]:
|
|
92
153
|
tree[dist+1].add(_node)
|
|
93
|
-
dist += 1
|
|
154
|
+
dist += 1 # increment distance
|
|
155
|
+
# layout new nodes
|
|
94
156
|
for i, node in enumerate(tree[dist]):
|
|
95
157
|
offset = i-0.5*len(tree[dist])
|
|
96
158
|
locs[node] = (dist + 0.1*offset, offset)
|
|
97
159
|
|
|
98
|
-
edges = set()
|
|
160
|
+
edges:set[Edge] = set()
|
|
99
161
|
for node in nodes:
|
|
100
162
|
for _node in (node.neighbours & nodes):
|
|
101
|
-
forward = (node, _node, node.weights[_node])
|
|
102
|
-
backward = (_node, node, _node.weights[node])
|
|
163
|
+
forward:Edge = (node, _node, node.weights[_node])
|
|
164
|
+
backward:Edge = (_node, node, _node.weights[node])
|
|
103
165
|
if forward not in edges and backward not in edges:
|
|
104
166
|
edges.add(forward)
|
|
105
167
|
|
|
106
168
|
fig, ax = plt.subplots(figsize=(8,8))
|
|
107
|
-
for node, loc in locs.items():
|
|
169
|
+
for node, loc in locs.items(): # plot nodes and labels
|
|
108
170
|
name = str(node) if labels is None else labels[node]
|
|
109
171
|
ax.plot(*loc, 'ko', ms=20)
|
|
110
172
|
ax.text(loc[0]+0.1, loc[1]+0.1, name)
|
|
111
173
|
|
|
112
|
-
for edge in edges:
|
|
174
|
+
for edge in edges: # plot edges and weights
|
|
113
175
|
a,b, weight = locs[edge[0]], locs[edge[1]], edge[2]
|
|
114
176
|
ax.plot(*[[i,j] for i,j in zip(a,b)],'k')
|
|
115
177
|
if weight is not None:
|
|
116
178
|
ax.text(*[i+(j-i)/2 for i,j in zip(a,b)], str(weight))
|
|
117
|
-
ax.axis("off")
|
|
179
|
+
ax.axis("off") # turn off axes
|
|
118
180
|
return fig
|
|
119
181
|
|
|
120
182
|
|
|
183
|
+
|
|
121
184
|
class GraphEnvironment(Environment):
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
185
|
+
""" An environment based on a graph structure. """
|
|
186
|
+
def __init__(self, graph:Graph | None = None, *args, **kwargs) -> None:
|
|
187
|
+
""" Create a graph environment."""
|
|
188
|
+
super().__init__(*args, **kwargs) # initialize base Environment
|
|
189
|
+
self.graph = graph if isinstance(graph, Graph) else Graph()
|
|
125
190
|
|
|
126
|
-
|
|
191
|
+
|
|
192
|
+
def add_node(self, node:Node) -> None:
|
|
193
|
+
""" Add a node to the graph environment.
|
|
194
|
+
|
|
195
|
+
:param node: Node to be added to the graph.
|
|
196
|
+
"""
|
|
127
197
|
self.graph.add_node(node)
|
|
128
198
|
|
|
129
|
-
|
|
130
|
-
|
|
199
|
+
|
|
200
|
+
@override
|
|
201
|
+
def percept(self, agent:Agent) -> list[tuple[Node, Numeric]]:
|
|
202
|
+
""" Return the percepts for the agent based on its location in the graph."""
|
|
203
|
+
node:Node = agent.location # type: ignore
|
|
131
204
|
if len(node.weights) == 0:
|
|
132
|
-
return node.neighbours
|
|
205
|
+
return [(n, 1) for n in node.neighbours]
|
|
133
206
|
else:
|
|
134
207
|
return [(n, node.weights[n]) for n in node.neighbours]
|
|
135
208
|
|
|
136
|
-
|
|
137
|
-
|
|
209
|
+
|
|
210
|
+
@override
|
|
211
|
+
def add_agent(self,
|
|
212
|
+
agent:Agent,
|
|
213
|
+
location:Location | None = None,
|
|
214
|
+
node:Node | None = None) -> None:
|
|
215
|
+
""" Add an agent to the graph environment at a specified location or node.
|
|
216
|
+
|
|
217
|
+
:param agent: Agent to be added to the environment.
|
|
218
|
+
:param location: Location where the agent should be added (default None).
|
|
219
|
+
:param node: Node where the agent should be added (default None).
|
|
220
|
+
"""
|
|
221
|
+
if not isinstance(agent, Agent): # type check
|
|
138
222
|
raise TypeError("f{self}: {agent} is not an Agent")
|
|
139
|
-
|
|
223
|
+
|
|
224
|
+
if node is not None: # assign to specified node
|
|
140
225
|
if not isinstance(node, Node):
|
|
141
226
|
print(f"{self}: {node} is not a valid Node")
|
|
142
|
-
|
|
227
|
+
|
|
228
|
+
if node in self.graph:
|
|
143
229
|
super().add_agent(agent, node)
|
|
144
230
|
else:
|
|
145
231
|
print(f"{self}: {node} is not in graph")
|
|
146
|
-
|
|
232
|
+
|
|
233
|
+
elif location is not None: # assign to node with matching location
|
|
147
234
|
_flag = True
|
|
148
|
-
|
|
235
|
+
|
|
236
|
+
for node in self.graph: # find node with matching location
|
|
149
237
|
if location == node.location:
|
|
150
238
|
super().add_agent(agent, node)
|
|
151
239
|
_flag = False
|
|
152
240
|
break
|
|
153
|
-
|
|
241
|
+
|
|
242
|
+
if _flag: # location not found
|
|
154
243
|
print(f"{self}: {location} was not found in environment")
|
|
155
|
-
|
|
244
|
+
|
|
245
|
+
else: # default to random node
|
|
156
246
|
super().add_agent(agent)
|
|
157
247
|
|
|
248
|
+
|
|
158
249
|
def show(self, *args, **kwargs):
|
|
250
|
+
""" Show a plot of the graph environment. """
|
|
159
251
|
with warnings.catch_warnings():
|
|
160
252
|
warnings.simplefilter("ignore")
|
|
161
253
|
self.graph.plot_nodes(*args, **kwargs).show()
|
|
162
254
|
|
|
255
|
+
|
|
163
256
|
@classmethod
|
|
164
|
-
def from_dict(
|
|
257
|
+
def from_dict(cls,
|
|
258
|
+
graph_dict: dict[str, Iterable[str | tuple[str,str] | int]]
|
|
259
|
+
)-> GraphEnvironment:
|
|
260
|
+
""" Create a GraphEnvironment from a dictionary representation of a graph."""
|
|
165
261
|
if "vertices" not in graph_dict and "edges" not in graph_dict:
|
|
166
262
|
raise ValueError(f"No vertices in json string {graph_dict}")
|
|
167
|
-
|
|
263
|
+
|
|
264
|
+
if "edges" not in graph_dict:
|
|
168
265
|
raise ValueError(f"No edges in json string {graph_dict}")
|
|
169
|
-
|
|
266
|
+
|
|
267
|
+
vertices = graph_dict[ # support 'nodes' or 'vertices'
|
|
268
|
+
'vertices' if 'vertices' in graph_dict else 'nodes']
|
|
170
269
|
edges = graph_dict['edges']
|
|
270
|
+
|
|
171
271
|
for edge in edges:
|
|
172
|
-
if len(edge) != 2:
|
|
173
|
-
raise ValueError(
|
|
174
|
-
|
|
175
|
-
|
|
272
|
+
if len(edge) != 2: # type: ignore
|
|
273
|
+
raise ValueError(
|
|
274
|
+
f"Edges must comprise two nodes, {edge} does not")
|
|
275
|
+
if any(v not in vertices for v in edge): # type: ignore
|
|
276
|
+
raise ValueError(
|
|
277
|
+
f"Edges must map between valid vertices, {edge} does not")
|
|
278
|
+
|
|
279
|
+
nodes = {v: Node(f"{v}") for v in vertices}
|
|
176
280
|
|
|
177
|
-
nodes = {v: Node(v) for v in vertices}
|
|
178
281
|
if 'weights' in graph_dict:
|
|
179
282
|
weights = graph_dict['weights']
|
|
180
283
|
else:
|
|
181
|
-
weights = [None]*len(edges)
|
|
284
|
+
weights = [None]*len(edges) # type: ignore
|
|
182
285
|
for edge, weight in zip(edges, weights):
|
|
183
|
-
a, b = edge
|
|
184
|
-
nodes[a].add_neighbour(nodes[b], weight)
|
|
286
|
+
a, b = edge # type: ignore
|
|
287
|
+
nodes[a].add_neighbour(nodes[b], weight) # add edge with weight
|
|
185
288
|
|
|
186
|
-
|
|
289
|
+
|
|
290
|
+
# create environment and add nodes
|
|
291
|
+
environment = cls()
|
|
187
292
|
for _,node in nodes.items():
|
|
188
293
|
environment.add_node(node)
|
|
294
|
+
|
|
189
295
|
return environment
|
|
190
296
|
|
|
191
297
|
@classmethod
|
|
192
|
-
def from_json(
|
|
193
|
-
|
|
298
|
+
def from_json(cls, json_str: str) -> GraphEnvironment:
|
|
299
|
+
""" Create a GraphEnvironment from a JSON string representation
|
|
300
|
+
of a graph."""
|
|
301
|
+
return cls.from_dict(json.loads(json_str))
|
|
194
302
|
|
|
195
303
|
|
|
196
304
|
class ShortestPathEnvironment(GraphEnvironment):
|
|
197
|
-
|
|
305
|
+
""" A graph environment for shortest path finding agents. """
|
|
306
|
+
def get_node(self, node: Node | Label) -> Node:
|
|
307
|
+
""" Retrieve a node from the graph by Node or label.
|
|
308
|
+
|
|
309
|
+
:param node: Node or label of the node to retrieve.
|
|
310
|
+
"""
|
|
198
311
|
if isinstance(node, Node):
|
|
199
312
|
assert node in self.graph
|
|
200
313
|
else:
|
|
@@ -202,21 +315,49 @@ class ShortestPathEnvironment(GraphEnvironment):
|
|
|
202
315
|
if node == vertex.label:
|
|
203
316
|
node = vertex
|
|
204
317
|
break
|
|
205
|
-
assert isinstance(
|
|
318
|
+
assert isinstance(node, Node)
|
|
206
319
|
return node
|
|
207
320
|
|
|
208
|
-
|
|
321
|
+
|
|
322
|
+
@override
|
|
323
|
+
def add_agent(self,
|
|
324
|
+
agent:ShortestPathAgent,
|
|
325
|
+
init: Node | Label,
|
|
326
|
+
target: Node | Label) -> None:
|
|
327
|
+
""" Add a shortest path agent to the environment.
|
|
328
|
+
|
|
329
|
+
:param agent: Agent to be added to the environment.
|
|
330
|
+
:param init: Initial node or label for the agent.
|
|
331
|
+
:param target: Target node or label for the agent, if applicable.
|
|
332
|
+
"""
|
|
333
|
+
# retrieve nodes from labels if necessary
|
|
209
334
|
init = self.get_node(init)
|
|
210
335
|
target = self.get_node(target)
|
|
336
|
+
# add agent to environment
|
|
211
337
|
super().add_agent(agent, node=init)
|
|
338
|
+
# initialise agent with init and target nodes
|
|
212
339
|
agent.initialise(init, target)
|
|
213
340
|
|
|
214
|
-
|
|
215
|
-
|
|
341
|
+
|
|
342
|
+
@property # override
|
|
343
|
+
def is_done(self) -> bool:
|
|
344
|
+
""" Check if all agents have delivered their paths. """
|
|
216
345
|
return (hasattr(self, "delivered") and self.delivered)
|
|
217
346
|
|
|
218
|
-
|
|
219
|
-
|
|
347
|
+
|
|
348
|
+
@override
|
|
349
|
+
def execute_action(self,
|
|
350
|
+
agent: ShortestPathAgent,
|
|
351
|
+
action: tuple[str, Node]) -> None:
|
|
352
|
+
""" Execute an action for the shortest path agent.
|
|
353
|
+
|
|
354
|
+
Commands:
|
|
355
|
+
- "explore": move to a neighbouring node
|
|
356
|
+
- "deliver": deliver the shortest path to the target node
|
|
357
|
+
|
|
358
|
+
:param agent: Agent executing the action.
|
|
359
|
+
:param action: Action to be executed, format (command, node).
|
|
360
|
+
"""
|
|
220
361
|
command, node = action
|
|
221
362
|
match command:
|
|
222
363
|
case "explore":
|
|
@@ -229,68 +370,76 @@ class ShortestPathEnvironment(GraphEnvironment):
|
|
|
229
370
|
|
|
230
371
|
|
|
231
372
|
class ShortestPathAgent(UtilityBasedAgent):
|
|
373
|
+
""" An agent that finds the shortest path in a graph environment.
|
|
374
|
+
|
|
375
|
+
Needs implementation of methods:
|
|
376
|
+
`program(self, percepts:list[tuple[Node, Numeric]]) -> tuple[str, Node]`
|
|
377
|
+
`utility(action:tuple[str, Node]) -> Numeric`
|
|
378
|
+
`explore(node:Node) -> None`
|
|
379
|
+
`deliver(node:Node) -> tuple[list[Node], Numeric]`
|
|
380
|
+
|
|
381
|
+
Has attributes:
|
|
382
|
+
`init`: Initial node of the agent.
|
|
383
|
+
`target`: Target node of the agent.
|
|
384
|
+
`location`: Current location of the agent.
|
|
385
|
+
`dist`: Dictionary mapping nodes to their distance from the initial node.
|
|
386
|
+
`prev`: Dictionary mapping nodes to their previous node in the shortest path.
|
|
387
|
+
"""
|
|
232
388
|
def __init__(self):
|
|
389
|
+
""" Initialise the shortest path agent. """
|
|
233
390
|
super().__init__()
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
self.
|
|
391
|
+
|
|
392
|
+
# attributes for shortest path finding
|
|
393
|
+
self.__init = self.__target = None
|
|
394
|
+
self.location:Node | None = None
|
|
395
|
+
self.dist:dict[Node, Numeric] = {}
|
|
396
|
+
self.prev:dict[Node, Node | None] = {}
|
|
397
|
+
self.visited:set[Node] = set()
|
|
398
|
+
|
|
237
399
|
|
|
238
400
|
@property
|
|
239
|
-
def at_goal(self):
|
|
401
|
+
def at_goal(self) -> bool:
|
|
402
|
+
""" Check if the agent is at the target node. """
|
|
240
403
|
return self.location is self.target
|
|
404
|
+
|
|
241
405
|
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
self.
|
|
406
|
+
@property
|
|
407
|
+
def init(self) -> Node | None:
|
|
408
|
+
""" Get the initial node of the agent. """
|
|
409
|
+
return self.__init
|
|
246
410
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
self.
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
def
|
|
255
|
-
|
|
256
|
-
curr = node
|
|
257
|
-
path.append(curr)
|
|
258
|
-
while curr is not self.init:
|
|
259
|
-
curr = self.prev[curr]
|
|
260
|
-
path.append(curr)
|
|
261
|
-
path = list(reversed(path))
|
|
262
|
-
print(f"{self}: delivering shortest path between {self.init} and {node}")
|
|
263
|
-
print(f" {path}")
|
|
264
|
-
print(f" length: {self.dist[node]}")
|
|
265
|
-
return path, self.dist[node]
|
|
266
|
-
|
|
267
|
-
def program(self, percepts):
|
|
268
|
-
if self.at_goal:
|
|
269
|
-
return ("deliver", self.target)
|
|
411
|
+
|
|
412
|
+
@property
|
|
413
|
+
def target(self) -> Node | None:
|
|
414
|
+
""" Get the target node of the agent. """
|
|
415
|
+
return self.__target
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
def initialise(self, node:Node, target:Node | None) -> None:
|
|
419
|
+
""" Initialise the agent with its starting and target nodes.
|
|
270
420
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
if alt < self.dist[neighbour]:
|
|
283
|
-
self.dist[neighbour] = alt
|
|
284
|
-
self.prev[neighbour] = self.location
|
|
285
|
-
|
|
286
|
-
action = self.maximise_utility(
|
|
287
|
-
[("explore", node)
|
|
288
|
-
for node in self.dist if (
|
|
289
|
-
node is not self.location and
|
|
290
|
-
node not in self.visited)])
|
|
421
|
+
:param node: Initial node for the agent.
|
|
422
|
+
:param target: Target node for the agent, if applicable.
|
|
423
|
+
"""
|
|
424
|
+
self.__init = self.location = node
|
|
425
|
+
self.__target = target
|
|
426
|
+
self.dist[node] = 0 # distance from initial node
|
|
427
|
+
self.prev[node] = None # previous node in path
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def explore(self, node:Node) -> None:
|
|
431
|
+
""" Explore a new node. The actuator of the agent. Needs to be implemented.
|
|
291
432
|
|
|
292
|
-
|
|
433
|
+
:param node: Node to be explored.
|
|
434
|
+
"""
|
|
435
|
+
raise NotImplementedError
|
|
436
|
+
|
|
293
437
|
|
|
294
|
-
def
|
|
295
|
-
|
|
296
|
-
|
|
438
|
+
def deliver(self, node:Node) -> tuple[list[Node], Numeric]:
|
|
439
|
+
""" Deliver the shortest path to the specified node,
|
|
440
|
+
and distance to node. Needs to be implemented
|
|
441
|
+
|
|
442
|
+
:param node: Node to deliver the shortest path to.
|
|
443
|
+
:return path, distance: Tuple of (shortest path as list of nodes, distance to node).
|
|
444
|
+
"""
|
|
445
|
+
raise NotImplementedError
|
co2114/search/things.py
CHANGED
|
@@ -1,32 +1,56 @@
|
|
|
1
|
-
from ..agent import
|
|
1
|
+
from ..agent.things import Thing, Agent, RationalAgent, ModelBasedAgent, Action
|
|
2
2
|
from numpy import inf
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
things.Thing,
|
|
6
|
-
things.Agent,
|
|
7
|
-
things.RationalAgent,
|
|
8
|
-
things.ModelBasedAgent)
|
|
4
|
+
from typing import Collection, override
|
|
9
5
|
|
|
10
6
|
|
|
11
7
|
class MazeRunner(Agent):
|
|
12
|
-
|
|
8
|
+
""" Agent that runs mazes, visualised as 👾 """
|
|
9
|
+
@override
|
|
10
|
+
def __repr__(self) -> str:
|
|
13
11
|
return "👾"
|
|
14
12
|
|
|
13
|
+
|
|
15
14
|
class GoalBasedAgent(ModelBasedAgent):
|
|
15
|
+
""" Base class for goal based agents
|
|
16
|
+
|
|
17
|
+
Requires implemnentation of methods:
|
|
18
|
+
`program(percepts:Collection[Thing]) -> Action`
|
|
19
|
+
`at_goal() -> bool`
|
|
20
|
+
"""
|
|
16
21
|
@property
|
|
17
|
-
def at_goal(self):
|
|
18
|
-
|
|
22
|
+
def at_goal(self) -> bool:
|
|
23
|
+
""" checks if agent is at goal, needs overriding """
|
|
24
|
+
raise NotImplementedError
|
|
25
|
+
|
|
19
26
|
|
|
20
27
|
class UtilityBasedAgent(GoalBasedAgent):
|
|
21
|
-
|
|
22
|
-
|
|
28
|
+
""" Base class for utility based agents
|
|
29
|
+
|
|
30
|
+
Requires implemnentation of methods:
|
|
31
|
+
`program(percepts:Collection[Thing]) -> Action`
|
|
32
|
+
`at_goal() -> bool`
|
|
33
|
+
`utility(action:Action) -> float`
|
|
34
|
+
"""
|
|
35
|
+
def maximise_utility(self, actions: Collection[Action]) -> Action:
|
|
36
|
+
""" calculates maximum utility from list of actions
|
|
37
|
+
|
|
38
|
+
:param actions: collection of possible actions
|
|
39
|
+
:return: action with maximum utility
|
|
40
|
+
"""
|
|
23
41
|
max_u = -inf # negative infinity
|
|
42
|
+
_flag = False # safety check
|
|
24
43
|
for action in actions:
|
|
25
44
|
u = self.utility(action)
|
|
26
45
|
if u > max_u:
|
|
27
46
|
max_u = u
|
|
28
47
|
output = action
|
|
29
|
-
|
|
48
|
+
_flag = True
|
|
49
|
+
if not _flag: raise ValueError("No valid actions provided")
|
|
50
|
+
|
|
51
|
+
return output # type: ignore
|
|
52
|
+
|
|
30
53
|
|
|
31
|
-
def utility(self, action):
|
|
32
|
-
|
|
54
|
+
def utility(self, action: Action) -> float:
|
|
55
|
+
""" calculates utility of an action, needs overriding """
|
|
56
|
+
raise NotImplementedError
|