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