pathos-ai 0.1.0__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.
pathos/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ from pathos.core.space import Space
2
+ from pathos.spaces.graph import GraphSpace
3
+ from pathos.spaces.csp import CSPSpace
4
+ from pathos.spaces.tour import TourSpace
5
+ from pathos.spaces.game import GameSpace
@@ -0,0 +1,7 @@
1
+ from pathos.algorithms.base import Algorithm
2
+ from pathos.algorithms.uninformed import BFS, DFS, IDDFS, UCS
3
+ from pathos.algorithms.informed import AStar, IDAstar, GreedyBestFirst, WeightedAStar, BidirectionalAStar
4
+ from pathos.algorithms.local import HillClimbing, TabuSearch, LocalBeamSearch
5
+ from pathos.algorithms.evolutionary import SimulatedAnnealing, GeneticAlgorithm, DifferentialEvolution
6
+ from pathos.algorithms.adversarial import Minimax, AlphaBeta, Negamax, MCTS
7
+ from pathos.algorithms.csp import Backtracking, ForwardChecking, AC3, MinConflicts
@@ -0,0 +1,240 @@
1
+ from __future__ import annotations
2
+ import time
3
+ import math
4
+ import random
5
+ from typing import Any
6
+ from pathos.algorithms.base import Algorithm
7
+ from pathos.core.capabilities import Capability
8
+ from pathos.core.result import SearchResult
9
+ from pathos.core.solver import register
10
+
11
+
12
+ @register
13
+ class Minimax(Algorithm):
14
+ """Minimax search for two-player zero-sum games.
15
+
16
+ Requires: successors, terminal, utility.
17
+
18
+ Attributes:
19
+ requires: Capability set needed.
20
+ power_rank: 40.
21
+ """
22
+
23
+ requires = frozenset({Capability.SUCCESSORS, Capability.TERMINAL, Capability.UTILITY})
24
+ power_rank = 40
25
+
26
+ def __init__(self, space: Any, max_depth: int = 100) -> None:
27
+ super().__init__(space)
28
+ self.max_depth = max_depth
29
+
30
+ def _minimax(self, state: Any, depth: int, is_max: bool) -> tuple[float, Any]:
31
+ if self.space._terminal(state) or depth == 0:
32
+ player = self.space._maximizing_player
33
+ return self.space._utility(state, player), state
34
+ moves = list(self.space._successors(state))
35
+ if not moves:
36
+ return self.space._utility(state, self.space._maximizing_player), state
37
+ if is_max:
38
+ best_val, best_state = -math.inf, None
39
+ for _, child in moves:
40
+ val, leaf = self._minimax(child, depth - 1, False)
41
+ if val > best_val:
42
+ best_val, best_state = val, leaf
43
+ return best_val, best_state
44
+ else:
45
+ best_val, best_state = math.inf, None
46
+ for _, child in moves:
47
+ val, leaf = self._minimax(child, depth - 1, True)
48
+ if val < best_val:
49
+ best_val, best_state = val, leaf
50
+ return best_val, best_state
51
+
52
+ def solve(self) -> SearchResult:
53
+ t0 = time.perf_counter()
54
+ val, state = self._minimax(self.space._initial, self.max_depth, True)
55
+ return SearchResult(state, None, val, "Minimax", 0, time.perf_counter() - t0, state is not None)
56
+
57
+
58
+ @register
59
+ class AlphaBeta(Algorithm):
60
+ """Alpha-Beta pruning — Minimax with branch pruning for efficiency.
61
+
62
+ Requires: successors, terminal, utility.
63
+
64
+ Attributes:
65
+ requires: Capability set needed.
66
+ power_rank: 45 (preferred over Minimax when available).
67
+ """
68
+
69
+ requires = frozenset({Capability.SUCCESSORS, Capability.TERMINAL, Capability.UTILITY})
70
+ power_rank = 45
71
+
72
+ def __init__(self, space: Any, max_depth: int = 100) -> None:
73
+ super().__init__(space)
74
+ self.max_depth = max_depth
75
+
76
+ def _ab(self, state: Any, depth: int, alpha: float, beta: float, is_max: bool) -> tuple[float, Any]:
77
+ if self.space._terminal(state) or depth == 0:
78
+ return self.space._utility(state, self.space._maximizing_player), state
79
+ moves = list(self.space._successors(state))
80
+ if not moves:
81
+ return self.space._utility(state, self.space._maximizing_player), state
82
+ best_state = None
83
+ if is_max:
84
+ val = -math.inf
85
+ for _, child in moves:
86
+ child_val, child_leaf = self._ab(child, depth - 1, alpha, beta, False)
87
+ if child_val > val:
88
+ val, best_state = child_val, child_leaf
89
+ alpha = max(alpha, val)
90
+ if alpha >= beta:
91
+ break
92
+ else:
93
+ val = math.inf
94
+ for _, child in moves:
95
+ child_val, child_leaf = self._ab(child, depth - 1, alpha, beta, True)
96
+ if child_val < val:
97
+ val, best_state = child_val, child_leaf
98
+ beta = min(beta, val)
99
+ if alpha >= beta:
100
+ break
101
+ return val, best_state
102
+
103
+ def solve(self) -> SearchResult:
104
+ t0 = time.perf_counter()
105
+ val, state = self._ab(self.space._initial, self.max_depth, -math.inf, math.inf, True)
106
+ return SearchResult(state, None, val, "AlphaBeta", 0, time.perf_counter() - t0, state is not None)
107
+
108
+
109
+ @register
110
+ class Negamax(Algorithm):
111
+ """Negamax — Minimax variant using score negation for multi-player support.
112
+
113
+ Requires: successors, terminal, utility.
114
+
115
+ Attributes:
116
+ requires: Capability set needed.
117
+ power_rank: 42.
118
+ """
119
+
120
+ requires = frozenset({Capability.SUCCESSORS, Capability.TERMINAL, Capability.UTILITY})
121
+ power_rank = 42
122
+
123
+ def __init__(self, space: Any, max_depth: int = 100) -> None:
124
+ super().__init__(space)
125
+ self.max_depth = max_depth
126
+
127
+ def _negamax(self, state: Any, depth: int, alpha: float, beta: float, player: int) -> tuple[float, Any]:
128
+ if self.space._terminal(state) or depth == 0:
129
+ val = self.space._utility(state, player)
130
+ return val, state
131
+ moves = list(self.space._successors(state))
132
+ if not moves:
133
+ return self.space._utility(state, player), state
134
+ best_val, best_state = -math.inf, moves[0][1]
135
+ next_player = (player + 1) % self.space._players
136
+ for _, child in moves:
137
+ child_val, _ = self._negamax(child, depth - 1, -beta, -alpha, next_player)
138
+ if -child_val > best_val:
139
+ best_val, best_state = -child_val, child
140
+ alpha = max(alpha, best_val)
141
+ if alpha >= beta:
142
+ break
143
+ return best_val, best_state
144
+
145
+ def solve(self) -> SearchResult:
146
+ t0 = time.perf_counter()
147
+ val, state = self._negamax(
148
+ self.space._initial, self.max_depth, -math.inf, math.inf,
149
+ self.space._maximizing_player
150
+ )
151
+ return SearchResult(state, None, val, "Negamax", 0, time.perf_counter() - t0, state is not None)
152
+
153
+
154
+ class _MCTSNode:
155
+ __slots__ = ("state", "parent", "children", "visits", "value", "untried")
156
+
157
+ def __init__(self, state: Any, parent: _MCTSNode | None, space: Any) -> None:
158
+ self.state = state
159
+ self.parent = parent
160
+ self.children: list[_MCTSNode] = []
161
+ self.visits = 0
162
+ self.value = 0.0
163
+ self.untried = list(space._successors(state)) if not space._terminal(state) else []
164
+
165
+ def uct_score(self, c: float = 1.414) -> float:
166
+ if self.visits == 0:
167
+ return math.inf
168
+ assert self.parent is not None
169
+ return self.value / self.visits + c * math.sqrt(math.log(self.parent.visits) / self.visits)
170
+
171
+ def best_child(self) -> _MCTSNode:
172
+ return max(self.children, key=lambda n: n.uct_score())
173
+
174
+ def is_fully_expanded(self) -> bool:
175
+ return len(self.untried) == 0
176
+
177
+
178
+ @register
179
+ class MCTS(Algorithm):
180
+ """Monte Carlo Tree Search — simulation-based game tree exploration.
181
+
182
+ Requires: successors, terminal, utility.
183
+
184
+ Attributes:
185
+ requires: Capability set needed.
186
+ power_rank: 43.
187
+ """
188
+
189
+ requires = frozenset({Capability.SUCCESSORS, Capability.TERMINAL, Capability.UTILITY})
190
+ power_rank = 43
191
+
192
+ def __init__(self, space: Any, iterations: int = 1000) -> None:
193
+ super().__init__(space)
194
+ self.iterations = iterations
195
+
196
+ def solve(self) -> SearchResult:
197
+ t0 = time.perf_counter()
198
+ root = _MCTSNode(self.space._initial, None, self.space)
199
+
200
+ for _ in range(self.iterations):
201
+ node = self._select(root)
202
+ if not self.space._terminal(node.state):
203
+ node = self._expand(node)
204
+ reward = self._simulate(node.state)
205
+ self._backprop(node, reward)
206
+
207
+ best = max(root.children, key=lambda n: n.visits) if root.children else root
208
+ return SearchResult(
209
+ best.state, None,
210
+ self.space._utility(best.state, self.space._maximizing_player),
211
+ "MCTS", self.iterations, time.perf_counter() - t0, True,
212
+ )
213
+
214
+ def _select(self, node: _MCTSNode) -> _MCTSNode:
215
+ while not self.space._terminal(node.state) and node.is_fully_expanded():
216
+ node = node.best_child()
217
+ return node
218
+
219
+ def _expand(self, node: _MCTSNode) -> _MCTSNode:
220
+ action, child_state = node.untried.pop()
221
+ child = _MCTSNode(child_state, node, self.space)
222
+ node.children.append(child)
223
+ return child
224
+
225
+ def _simulate(self, state: Any) -> float:
226
+ depth = 0
227
+ while not self.space._terminal(state) and depth < 50:
228
+ moves = list(self.space._successors(state))
229
+ if not moves:
230
+ break
231
+ _, state = random.choice(moves)
232
+ depth += 1
233
+ result: float = self.space._utility(state, self.space._maximizing_player)
234
+ return result
235
+
236
+ def _backprop(self, node: _MCTSNode | None, reward: float) -> None:
237
+ while node is not None:
238
+ node.visits += 1
239
+ node.value += reward
240
+ node = node.parent
@@ -0,0 +1,65 @@
1
+ from __future__ import annotations
2
+ from abc import ABC, abstractmethod
3
+ from typing import Any, TYPE_CHECKING
4
+ from pathos.core.capabilities import Capability
5
+ from pathos.core.result import SearchResult
6
+
7
+ if TYPE_CHECKING:
8
+ from pathos.core.space import Space
9
+
10
+
11
+ class Algorithm(ABC):
12
+ requires: frozenset[Capability] = frozenset()
13
+ power_rank: int = 0 # higher = preferred when multiple are compatible
14
+
15
+ def __init__(self, space: Space) -> None:
16
+ missing = self.requires - space.capabilities
17
+ if missing:
18
+ raise ValueError(
19
+ f"{self.__class__.__name__} requires capabilities: "
20
+ f"{', '.join(c.name for c in missing)}"
21
+ )
22
+ self.space = space
23
+ self._n_workers: int = space._n_workers
24
+
25
+ @abstractmethod
26
+ def solve(self) -> SearchResult:
27
+ ...
28
+
29
+ @classmethod
30
+ def compatible_with(cls, space: Space) -> bool:
31
+ return cls.requires <= space.capabilities
32
+
33
+ @classmethod
34
+ def score_for(cls, space: Space) -> float:
35
+ """Context-aware preference score for `space`. Higher = preferred.
36
+
37
+ The auto-solver picks the compatible algorithm with the highest
38
+ `score_for(space)`. Defaults to `float(power_rank)`, so any
39
+ algorithm that doesn't care about problem context keeps its
40
+ existing fixed-rank behavior.
41
+
42
+ Override to encode self-knowledge that isn't captured by the
43
+ coarse static rank — for example, "I work well on small problems
44
+ but lose to a sibling on large ones", or "without `@successors`
45
+ I degenerate to a no-op, so cede to siblings that don't need it".
46
+
47
+ Returning a value below `power_rank` lets the algorithm cede to
48
+ better-suited siblings without changing its global ordering.
49
+ """
50
+ return float(cls.power_rank)
51
+
52
+ def _goal_reached(self, state: Any) -> bool:
53
+ """Whether `state` satisfies the space's declared goal predicate.
54
+
55
+ If the space has no GOAL capability, returns True — the algorithm
56
+ terminated by its own stopping rule and there's no goal to check.
57
+ Otherwise consults `space._goal(state)`.
58
+
59
+ Used by local-search / metaheuristic algorithms (HC, Tabu, Beam, SA,
60
+ GA, DE) so they can't report `found=True` on a goal-bearing problem
61
+ when the best state they reached doesn't actually satisfy the goal.
62
+ """
63
+ if Capability.GOAL in self.space.capabilities:
64
+ return bool(self.space._goal(state))
65
+ return True
@@ -0,0 +1,196 @@
1
+ from __future__ import annotations
2
+ import time
3
+ import random
4
+ from typing import Any, cast
5
+ from pathos.algorithms.base import Algorithm
6
+ from pathos.core.capabilities import Capability
7
+ from pathos.core.result import SearchResult
8
+ from pathos.core.solver import register
9
+
10
+
11
+ def _is_csp_shaped(space: Any) -> bool:
12
+ """True if state is a partial-assignment dict — the precondition for the
13
+ incremental-extension recursion used by Backtracking / ForwardChecking /
14
+ MinConflicts. Without this guard they're offered for any
15
+ successors+goal space and recurse forever (8-puzzle: RecursionError) or
16
+ silent-fail (MinConflicts on plain Space)."""
17
+ return isinstance(space._initial, dict)
18
+
19
+
20
+ @register
21
+ class Backtracking(Algorithm):
22
+ """Backtracking search — systematic recursive CSP solver.
23
+
24
+ Uses the space's successors as CSP expansion (typically via CSPSpace).
25
+
26
+ Requires: successors, goal.
27
+
28
+ Attributes:
29
+ requires: Capability set needed.
30
+ power_rank: 9.
31
+ """
32
+
33
+ requires = frozenset({Capability.SUCCESSORS, Capability.GOAL})
34
+ power_rank = 9
35
+
36
+ @classmethod
37
+ def compatible_with(cls, space: Any) -> bool:
38
+ return super().compatible_with(space) and _is_csp_shaped(space)
39
+
40
+ def _bt(self, state: Any, expanded: list[int]) -> Any | None:
41
+ if self.space._goal(state):
42
+ return state
43
+ for _, child in self.space._successors(state):
44
+ expanded[0] += 1
45
+ result = self._bt(child, expanded)
46
+ if result is not None:
47
+ return result
48
+ return None
49
+
50
+ def solve(self) -> SearchResult:
51
+ t0 = time.perf_counter()
52
+ expanded = [0]
53
+ result = self._bt(self.space._initial, expanded)
54
+ elapsed = time.perf_counter() - t0
55
+ if result is not None:
56
+ return SearchResult(result, None, None, "Backtracking", expanded[0], elapsed, True)
57
+ return SearchResult.not_found("Backtracking", expanded[0], elapsed)
58
+
59
+
60
+ @register
61
+ class ForwardChecking(Algorithm):
62
+ """Forward Checking — Backtracking with look-ahead pruning.
63
+
64
+ Before recursing, checks that each child has at least one valid successor.
65
+
66
+ The current look-ahead is shallow (a child is pruned only when it has no
67
+ successors and is not itself a goal), so node counts match plain
68
+ Backtracking and the constant factor is higher. Until real domain-level
69
+ pruning is implemented, FC is ranked below Backtracking so the auto-
70
+ solver picks the faster of the two.
71
+
72
+ Requires: successors, goal.
73
+
74
+ Attributes:
75
+ requires: Capability set needed.
76
+ power_rank: 8 (below Backtracking's 9).
77
+ """
78
+
79
+ requires = frozenset({Capability.SUCCESSORS, Capability.GOAL})
80
+ power_rank = 8
81
+
82
+ @classmethod
83
+ def compatible_with(cls, space: Any) -> bool:
84
+ return super().compatible_with(space) and _is_csp_shaped(space)
85
+
86
+ def _fc(self, state: Any, expanded: list[int]) -> Any | None:
87
+ if self.space._goal(state):
88
+ return state
89
+ children = list(self.space._successors(state))
90
+ if not children:
91
+ return None
92
+ for _, child in children:
93
+ expanded[0] += 1
94
+ # check if any successor exists from child (look-ahead)
95
+ future = list(self.space._successors(child))
96
+ if future or self.space._goal(child):
97
+ result = self._fc(child, expanded)
98
+ if result is not None:
99
+ return result
100
+ return None
101
+
102
+ def solve(self) -> SearchResult:
103
+ t0 = time.perf_counter()
104
+ expanded = [0]
105
+ result = self._fc(self.space._initial, expanded)
106
+ elapsed = time.perf_counter() - t0
107
+ if result is not None:
108
+ return SearchResult(result, None, None, "ForwardChecking", expanded[0], elapsed, True)
109
+ return SearchResult.not_found("ForwardChecking", expanded[0], elapsed)
110
+
111
+
112
+ class AC3(Algorithm):
113
+ """AC-3 arc consistency — domain-pruning preprocessor for CSPs.
114
+
115
+ AC-3 is not a stand-alone CSP solver: it iterates over arcs and prunes
116
+ domain values that can't satisfy any binary constraint, then hands off
117
+ a (possibly smaller) problem to Backtracking / ForwardChecking. It is
118
+ intentionally NOT @register'd so the auto-selector won't pick it as a
119
+ terminal solver — its `solve()` returns the pruned domains, not an
120
+ assignment, so power_rank=22 would mis-rank it above MinConflicts.
121
+
122
+ Use directly: `AC3(csp_space).solve()` returns a SearchResult whose
123
+ `.solution` is the pruned-domain dict. Production code typically
124
+ composes AC-3 with a Backtracking-style solver — see issue tracker
125
+ for the planned `pathos.algorithms.preprocessors` API.
126
+ """
127
+ requires = frozenset({Capability.VARIABLES, Capability.DOMAINS, Capability.CONSTRAINTS})
128
+ power_rank = 22
129
+
130
+ def solve(self) -> SearchResult:
131
+ from pathos.spaces.csp import CSPSpace
132
+ t0 = time.perf_counter()
133
+ # This is invoked by CSPSpace which sets up the variables/domains/constraints
134
+ csp = cast(CSPSpace, self.space)
135
+ variables = csp._variables()
136
+ domains: dict[Any, list[Any]] = {v: list(csp._domain(v)) for v in variables}
137
+ arcs = [(xi, xj) for xi in variables for xj in variables if xi != xj]
138
+ queue = list(arcs)
139
+
140
+ while queue:
141
+ xi, xj = queue.pop(0)
142
+ if self._revise(domains, xi, xj, csp):
143
+ if not domains[xi]:
144
+ return SearchResult.not_found("AC3", 0, time.perf_counter() - t0)
145
+ for xk in variables:
146
+ if xk != xi and xk != xj:
147
+ queue.append((xk, xi))
148
+
149
+ return SearchResult(domains, None, None, "AC3", 0, time.perf_counter() - t0, True)
150
+
151
+ def _revise(self, domains: dict[Any, list[Any]], xi: Any, xj: Any, csp: Any) -> bool:
152
+ revised = False
153
+ for x in domains[xi][:]:
154
+ if not any(csp._constraints({xi: x, xj: y}) for y in domains[xj]):
155
+ domains[xi].remove(x)
156
+ revised = True
157
+ return revised
158
+
159
+
160
+ @register
161
+ class MinConflicts(Algorithm):
162
+ """Min-Conflicts heuristic — local repair CSP solver.
163
+
164
+ Repeatedly selects the neighbor that minimizes constraint violations.
165
+
166
+ Requires: successors, goal, evaluate.
167
+
168
+ Attributes:
169
+ requires: Capability set needed.
170
+ power_rank: 19.
171
+ """
172
+
173
+ requires = frozenset({Capability.SUCCESSORS, Capability.GOAL, Capability.EVALUATE})
174
+ power_rank = 19
175
+
176
+ @classmethod
177
+ def compatible_with(cls, space: Any) -> bool:
178
+ return super().compatible_with(space) and _is_csp_shaped(space)
179
+
180
+ def __init__(self, space: Any, max_iter: int = 1000) -> None:
181
+ super().__init__(space)
182
+ self.max_iter = max_iter
183
+
184
+ def solve(self) -> SearchResult:
185
+ t0 = time.perf_counter()
186
+ current = self.space._initial
187
+
188
+ for i in range(self.max_iter):
189
+ if self.space._goal(current):
190
+ return SearchResult(current, None, 0.0, "MinConflicts", i, time.perf_counter() - t0, True)
191
+ neighbors = list(self.space._successors(current))
192
+ if not neighbors:
193
+ break
194
+ _, current = min(neighbors, key=lambda ac: self.space._evaluate(ac[1]))
195
+
196
+ return SearchResult.not_found("MinConflicts", self.max_iter, time.perf_counter() - t0)