treequest 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.
treequest/__init__.py ADDED
@@ -0,0 +1,15 @@
1
+ from .algos.ab_mcts_a.algo import ABMCTSA
2
+ from .algos.ab_mcts_m.algo import ABMCTSM
3
+ from .algos.base import Algorithm
4
+ from .algos.standard_mcts import StandardMCTS
5
+ from .algos.tree_of_thought_bfs import TreeOfThoughtsBFSAlgo
6
+ from .ranker import top_k
7
+
8
+ __all__ = [
9
+ "StandardMCTS",
10
+ "top_k",
11
+ "TreeOfThoughtsBFSAlgo",
12
+ "ABMCTSA",
13
+ "ABMCTSM",
14
+ "Algorithm",
15
+ ]
@@ -0,0 +1,383 @@
1
+ import copy
2
+ from collections import defaultdict
3
+ from collections.abc import Mapping
4
+ from dataclasses import dataclass, field
5
+ from logging import getLogger
6
+ from typing import Dict, Generic, List, Literal, Optional, Tuple, TypeVar, Union
7
+
8
+ from treequest.algos.ab_mcts_a.prob_state import NodeProbState, PriorConfig
9
+ from treequest.algos.base import Algorithm
10
+ from treequest.algos.tree import Node, Tree
11
+ from treequest.types import GenerateFnType, StateScoreType
12
+
13
+ # Type variable for state
14
+ StateT = TypeVar("StateT")
15
+
16
+ logger = getLogger(__name__)
17
+
18
+
19
+ @dataclass
20
+ class ABMCTSAStateManager(Generic[StateT]):
21
+ """Manager for ABMCTSAState instances associated with expand_idx values."""
22
+
23
+ states: Dict[int, NodeProbState] = field(default_factory=dict)
24
+ default_prior_config: Optional["PriorConfig"] = None
25
+ default_reward_average_priors: Optional[Union[float, Dict[str, float]]] = None
26
+ default_model_selection_strategy: str = "multiarm_bandit_thompson"
27
+
28
+ def __contains__(self, expand_idx: int) -> bool:
29
+ """Check if a thompson state exists for the given expand_idx."""
30
+ return expand_idx in self.states
31
+
32
+ def __len__(self) -> int:
33
+ return len(self.states)
34
+
35
+ def get(self, node: Node) -> Optional[NodeProbState]:
36
+ """Get thompson state for the given expand_idx if it exists."""
37
+ return self.states.get(node.expand_idx)
38
+
39
+ def create(
40
+ self,
41
+ node: Node,
42
+ actions: List[str],
43
+ prior_config: Optional["PriorConfig"] = None,
44
+ reward_average_priors: Optional[Union[float, Dict[str, float]]] = None,
45
+ model_selection_strategy: Optional[str] = None,
46
+ ) -> NodeProbState:
47
+ """Create a new thompson state for the given expand_idx with optional prior configuration."""
48
+ # Use provided configs or fall back to defaults
49
+ prior_config = prior_config or self.default_prior_config
50
+ reward_average_priors = (
51
+ reward_average_priors or self.default_reward_average_priors
52
+ )
53
+ model_selection_strategy = (
54
+ model_selection_strategy or self.default_model_selection_strategy
55
+ )
56
+
57
+ state = NodeProbState(
58
+ actions=actions,
59
+ prior_config=prior_config,
60
+ reward_average_priors=reward_average_priors,
61
+ model_selection_strategy=model_selection_strategy,
62
+ )
63
+ self.states[node.expand_idx] = state
64
+ return state
65
+
66
+ def get_or_create(
67
+ self,
68
+ node: Node,
69
+ actions: List[str],
70
+ prior_config: Optional["PriorConfig"] = None,
71
+ reward_average_priors: Optional[Union[float, Dict[str, float]]] = None,
72
+ model_selection_strategy: Optional[str] = None,
73
+ ) -> NodeProbState:
74
+ """Get existing thompson state or create a new one if it doesn't exist."""
75
+ if node.expand_idx in self.states:
76
+ return self.states[node.expand_idx]
77
+ return self.create(
78
+ node,
79
+ actions,
80
+ prior_config=prior_config,
81
+ reward_average_priors=reward_average_priors,
82
+ model_selection_strategy=model_selection_strategy,
83
+ )
84
+
85
+
86
+ def build_default_dict_of_list() -> Dict[str, List[float]]:
87
+ """
88
+ For pickle we define it at the top-module level as named function.
89
+ Ref: https://docs.python.org/3.13/library/pickle.html#what-can-be-pickled-and-unpickled
90
+ """
91
+ return defaultdict(list)
92
+
93
+
94
+ @dataclass
95
+ class ABMCTSAAlgoState(Generic[StateT]):
96
+ """State for ABMCTSA algorithm."""
97
+
98
+ tree: Tree[StateT]
99
+ thompson_states: ABMCTSAStateManager = field(default_factory=ABMCTSAStateManager)
100
+ all_rewards_store: Dict[str, List[float]] = field(
101
+ default_factory=build_default_dict_of_list
102
+ )
103
+
104
+
105
+ class ABMCTSA(Algorithm[StateT, ABMCTSAAlgoState[StateT]]):
106
+ """
107
+ Adaptive Monte Carlo Tree Search algorithm with Node Aggregation.
108
+
109
+ This algorithm uses Thompson Sampling to decide whether to generate a new node (GEN)
110
+ or continue exploring from an existing one (CONT).
111
+ """
112
+
113
+ def __init__(
114
+ self,
115
+ dist_type: Literal["gaussian", "beta"] = "gaussian",
116
+ reward_average_priors: Optional[Union[float, Dict[str, float]]] = None,
117
+ prior_config: Optional[PriorConfig] = None,
118
+ model_selection_strategy: str = "multiarm_bandit_thompson",
119
+ ):
120
+ """
121
+ Initialize the AB-MCTS-A algorithm.
122
+
123
+ Args:
124
+ dist_type: Type of Probability distribution. Either "gaussian" or "beta".
125
+ reward_average_priors: Optional prior reward average values (global or per-action)
126
+ prior_config: Optional prior configuration for Thompson sampling distributions. If specified, dist_type will not be used.
127
+ model_selection_strategy: Strategy for model selection. One of:
128
+ - "stack": Perform separate fits for each model (traditional approach)
129
+ - "multiarm_bandit_thompson": Use Thompson Sampling for joint selection
130
+ - "multiarm_bandit_ucb": Use UCB for joint selection
131
+ """
132
+ if prior_config is None:
133
+ prior_config = PriorConfig(dist_type=dist_type)
134
+ else:
135
+ if dist_type != prior_config.dist_type:
136
+ logger.warning(
137
+ f"dist_type argument {dist_type} is different from the one specified by prior_config {prior_config.dist_type}. {dist_type} is ignored, and {prior_config.dist_type} will be used."
138
+ )
139
+
140
+ self.prior_config = prior_config
141
+ self.reward_average_priors = reward_average_priors
142
+
143
+ # Strategy for model selection:
144
+ # "stack": Perform separate fits for each model (traditional approach)
145
+ # "multiarm_bandit_thompson": Use Thompson Sampling for joint selection
146
+ # "multiarm_bandit_ucb": Use UCB for joint selection
147
+ if model_selection_strategy not in [
148
+ "stack",
149
+ "multiarm_bandit_thompson",
150
+ "multiarm_bandit_ucb",
151
+ ]:
152
+ raise ValueError(
153
+ f"Invalid model_selection_strategy: {model_selection_strategy}. "
154
+ f"Must be one of: 'stack', 'multiarm_bandit_thompson', 'multiarm_bandit_ucb'"
155
+ )
156
+ self.model_selection_strategy = model_selection_strategy
157
+
158
+ def init_tree(self) -> ABMCTSAAlgoState:
159
+ """
160
+ Initialize the algorithm state with an empty tree.
161
+
162
+ Returns:
163
+ Initial algorithm state
164
+ """
165
+ tree: Tree = Tree.with_root_node()
166
+ state_manager = ABMCTSAStateManager[StateT](
167
+ default_prior_config=self.prior_config,
168
+ default_reward_average_priors=self.reward_average_priors,
169
+ default_model_selection_strategy=self.model_selection_strategy,
170
+ )
171
+ return ABMCTSAAlgoState(tree=tree, thompson_states=state_manager)
172
+
173
+ def step(
174
+ self,
175
+ state: ABMCTSAAlgoState,
176
+ generate_fn: Mapping[str, GenerateFnType[StateT]],
177
+ inplace: bool = False,
178
+ ) -> ABMCTSAAlgoState:
179
+ """
180
+ Perform one step of the Thompson Sampling MCTS algorithm.
181
+
182
+ Args:
183
+ state: Current algorithm state
184
+ generate_fn: Mapping of action names to generation functions
185
+
186
+ Returns:
187
+ Updated algorithm state
188
+ """
189
+ if not inplace:
190
+ state = copy.deepcopy(state)
191
+
192
+ # initialize all_rewards_store
193
+ if len(state.all_rewards_store) == 0:
194
+ for action in generate_fn:
195
+ state.all_rewards_store[action] = []
196
+
197
+ # If the tree is empty (only root), expand the root
198
+ if not state.tree.root.children:
199
+ self._expand_node(state, state.tree.root, generate_fn)
200
+ return state
201
+
202
+ # Run one simulation step
203
+ node = state.tree.root
204
+
205
+ # Selection phase: traverse tree until we reach a leaf node or need to create a new node
206
+ while node.children:
207
+ node, action_used = self._select_child(state, node, generate_fn)
208
+
209
+ # If action is not None, it means we've generated a new node
210
+ if action_used is not None:
211
+ return state
212
+
213
+ # Expansion phase: expand leaf node
214
+ self._expand_node(state, node, generate_fn)
215
+
216
+ return state
217
+
218
+ def _select_child(
219
+ self,
220
+ state: ABMCTSAAlgoState,
221
+ node: Node,
222
+ generate_fn: Mapping[str, GenerateFnType[StateT]],
223
+ ) -> Tuple[Node, Optional[str]]:
224
+ """
225
+ Select a child node using Thompson Sampling.
226
+
227
+ Args:
228
+ state: Current algorithm state
229
+ node: Node to select child from
230
+ generate_fn: Mapping of action names to generation functions
231
+
232
+ Returns:
233
+ Tuple of (selected node, action if new node was generated)
234
+ """
235
+ # Get or create thompson state for this node
236
+ thompson_state = state.thompson_states.get_or_create(
237
+ node,
238
+ list(generate_fn.keys()),
239
+ )
240
+
241
+ # Ask for next node or action using Thompson Sampling
242
+ selection = thompson_state.select_next(state.all_rewards_store)
243
+
244
+ # If string returned, we need to generate a new node with that action
245
+ if isinstance(selection, str):
246
+ new_node = self._generate_new_child(state, node, generate_fn, selection)
247
+ return new_node, selection
248
+ else:
249
+ # Otherwise, we return the existing child with that index
250
+ if selection >= len(node.children):
251
+ raise RuntimeError(
252
+ f"Something went wrong in ABMCTSA algorithm: selected index {selection} is out of bounds."
253
+ )
254
+ return node.children[selection], None
255
+
256
+ def _expand_node(
257
+ self,
258
+ state: ABMCTSAAlgoState,
259
+ node: Node,
260
+ generate_fn: Mapping[str, GenerateFnType[StateT]],
261
+ ) -> Tuple[Node, str]:
262
+ """
263
+ Expand a leaf node by generating a new child.
264
+
265
+ Args:
266
+ state: Current algorithm state
267
+ node: Node to expand
268
+ generate_fn: Mapping of action names to generation functions
269
+
270
+ Returns:
271
+ Tuple of (new node, action used)
272
+ """
273
+ # Create thompson state for this node if it doesn't exist
274
+ thompson_state = state.thompson_states.get_or_create(
275
+ node,
276
+ list(generate_fn.keys()),
277
+ )
278
+
279
+ # Get action to use for generating child
280
+ selection = thompson_state.select_next(state.all_rewards_store)
281
+
282
+ # Ensure we get a string action name, not an index
283
+ if not isinstance(selection, str):
284
+ raise RuntimeError(
285
+ f"Something went wrong in ABMCTSA algorithm: selection should always be str when the expansion is from the leaf node, whle got {selection}"
286
+ )
287
+
288
+ new_node = self._generate_new_child(state, node, generate_fn, selection)
289
+ return new_node, selection
290
+
291
+ def _generate_new_child(
292
+ self,
293
+ state: ABMCTSAAlgoState,
294
+ node: Node,
295
+ generate_fn: Mapping[str, GenerateFnType[StateT]],
296
+ action: str,
297
+ ) -> Node:
298
+ """
299
+ Generate a new child node using the specified action.
300
+
301
+ Args:
302
+ state: Current algorithm state
303
+ node: Parent node
304
+ generate_fn: Mapping of action names to generation functions
305
+ action: Name of action to use for generation
306
+
307
+ Returns:
308
+ Newly created node
309
+ """
310
+ # Generate new state and score using the selected action
311
+ node_state = None if node.is_root() else node.state
312
+ new_state, new_score = generate_fn[action](node_state)
313
+
314
+ # Add new node to the tree
315
+ new_node = state.tree.add_node((new_state, new_score), node)
316
+
317
+ # Update Thompson state with the new node
318
+ thompson_state = state.thompson_states.get(node)
319
+ if thompson_state:
320
+ thompson_state.register_new_child_node(
321
+ action, new_node, self.model_selection_strategy
322
+ )
323
+ else:
324
+ raise RuntimeError(
325
+ f"Internal Error in ABMCTSA: thompson_state should not be None for node {node}"
326
+ )
327
+
328
+ # Backpropagate the score through the parents
329
+ self._backpropagate(state, new_node, new_score, action)
330
+
331
+ return new_node
332
+
333
+ def _backpropagate(
334
+ self, state: ABMCTSAAlgoState, node: Node, score: float, action: str
335
+ ) -> None:
336
+ """
337
+ Update Thompson Sampling statistics for all nodes in the path from node to root.
338
+
339
+ Args:
340
+ state: Current algorithm state
341
+ node: Leaf node to start backpropagation from
342
+ score: Score to backpropagate
343
+ action: The action which have generated node
344
+ """
345
+ # Update all_rewards_score for multiarm bandit use
346
+ state.all_rewards_store[action].append(score)
347
+
348
+ # NOTE: For the newly created node, we always update the score for GEN node
349
+ assert node.parent is not None
350
+ thompson_state = state.thompson_states.get(node.parent)
351
+ if thompson_state is None:
352
+ raise RuntimeError(
353
+ "Internal Error in ABMCTSA: ThompsonState should have been already initialized"
354
+ )
355
+ thompson_state.update_action_reward(action=action, reward=score)
356
+
357
+ current: Optional[Node] = node.parent
358
+ while current is not None and current.parent is not None:
359
+ thompson_state = state.thompson_states.get(current.parent)
360
+ if thompson_state is None:
361
+ raise RuntimeError(
362
+ "Internal Error in ABMCTSA: ThompsonState should have been already initialized"
363
+ )
364
+
365
+ # Update the Thompson state with the score
366
+ thompson_state.update_node_reward(current, score)
367
+
368
+ # Move up to the parent
369
+ current = current.parent
370
+
371
+ def get_state_score_pairs(
372
+ self, state: ABMCTSAAlgoState
373
+ ) -> List[StateScoreType[StateT]]:
374
+ """
375
+ Get all the state-score pairs from the tree.
376
+
377
+ Args:
378
+ state: Current algorithm state
379
+
380
+ Returns:
381
+ List of (state, score) pairs
382
+ """
383
+ return state.tree.get_state_score_pairs()