treequest 0.1.2__tar.gz → 0.2.0__tar.gz

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 (58) hide show
  1. {treequest-0.1.2 → treequest-0.2.0}/.gitignore +3 -0
  2. {treequest-0.1.2 → treequest-0.2.0}/PKG-INFO +52 -1
  3. {treequest-0.1.2 → treequest-0.2.0}/README.md +49 -0
  4. treequest-0.2.0/docs/PROFILING.md +98 -0
  5. treequest-0.2.0/images/batch/abmcts_m_batch_size_1.jpg +0 -0
  6. treequest-0.2.0/images/batch/abmcts_m_batch_size_10.jpg +0 -0
  7. treequest-0.2.0/images/batch/abmcts_m_batch_size_2.jpg +0 -0
  8. treequest-0.2.0/images/batch/abmcts_m_batch_size_5.jpg +0 -0
  9. {treequest-0.1.2 → treequest-0.2.0}/pyproject.toml +3 -1
  10. {treequest-0.1.2 → treequest-0.2.0}/src/treequest/__init__.py +3 -3
  11. {treequest-0.1.2 → treequest-0.2.0}/src/treequest/algos/ab_mcts_a/algo.py +107 -102
  12. {treequest-0.1.2 → treequest-0.2.0}/src/treequest/algos/ab_mcts_m/algo.py +152 -91
  13. treequest-0.2.0/src/treequest/algos/ab_mcts_m/numpyro_utils.py +13 -0
  14. {treequest-0.1.2 → treequest-0.2.0}/src/treequest/algos/ab_mcts_m/pymc_interface.py +4 -12
  15. {treequest-0.1.2 → treequest-0.2.0}/src/treequest/algos/base.py +27 -0
  16. {treequest-0.1.2 → treequest-0.2.0}/src/treequest/algos/best_first_search.py +76 -27
  17. {treequest-0.1.2 → treequest-0.2.0}/src/treequest/algos/multi_armed_bandit_ucb.py +64 -28
  18. {treequest-0.1.2 → treequest-0.2.0}/src/treequest/algos/standard_mcts.py +61 -37
  19. {treequest-0.1.2 → treequest-0.2.0}/src/treequest/algos/tree_of_thought_bfs.py +66 -33
  20. treequest-0.2.0/src/treequest/trial.py +240 -0
  21. treequest-0.2.0/tests/profiling/ab_mcts_a.py +101 -0
  22. treequest-0.2.0/tests/profiling/ab_mcts_m.py +114 -0
  23. {treequest-0.1.2 → treequest-0.2.0}/tests/test_ab_mcts_a.py +16 -51
  24. treequest-0.2.0/tests/test_ask_batch.py +111 -0
  25. treequest-0.2.0/tests/test_e2e_batch_flow.py +71 -0
  26. treequest-0.2.0/tests/test_interleaved_ask_tell.py +123 -0
  27. treequest-0.2.0/tests/test_next_nodes_behavior.py +139 -0
  28. treequest-0.2.0/tests/test_reflection_logic.py +166 -0
  29. {treequest-0.1.2 → treequest-0.2.0}/tests/test_tot_bfs.py +8 -7
  30. treequest-0.2.0/tests/test_trial_tell_idempotent.py +77 -0
  31. treequest-0.2.0/uv.lock +1493 -0
  32. treequest-0.1.2/images/figure1.png +0 -0
  33. treequest-0.1.2/uv.lock +0 -1345
  34. {treequest-0.1.2 → treequest-0.2.0}/.github/workflows/checks.yml +0 -0
  35. {treequest-0.1.2 → treequest-0.2.0}/.github/workflows/release.yml +0 -0
  36. {treequest-0.1.2 → treequest-0.2.0}/.github/workflows/tests.yml +0 -0
  37. {treequest-0.1.2 → treequest-0.2.0}/.pre-commit-config.yaml +0 -0
  38. {treequest-0.1.2 → treequest-0.2.0}/.python-version +0 -0
  39. {treequest-0.1.2 → treequest-0.2.0}/CONTRIBUTING.md +0 -0
  40. {treequest-0.1.2 → treequest-0.2.0}/LICENSE +0 -0
  41. {treequest-0.1.2 → treequest-0.2.0}/pytest.ini +0 -0
  42. {treequest-0.1.2 → treequest-0.2.0}/src/treequest/algos/ab_mcts_a/prob_state.py +0 -0
  43. {treequest-0.1.2 → treequest-0.2.0}/src/treequest/algos/ab_mcts_m/_ab_mcts_m_imports.py +0 -0
  44. {treequest-0.1.2 → treequest-0.2.0}/src/treequest/algos/tree.py +0 -0
  45. {treequest-0.1.2 → treequest-0.2.0}/src/treequest/imports.py +0 -0
  46. {treequest-0.1.2 → treequest-0.2.0}/src/treequest/py.typed +0 -0
  47. {treequest-0.1.2 → treequest-0.2.0}/src/treequest/ranker.py +0 -0
  48. {treequest-0.1.2 → treequest-0.2.0}/src/treequest/types.py +0 -0
  49. {treequest-0.1.2 → treequest-0.2.0}/src/treequest/visualization.py +0 -0
  50. {treequest-0.1.2 → treequest-0.2.0}/tests/__init__.py +0 -0
  51. {treequest-0.1.2 → treequest-0.2.0}/tests/test_ab_mcts_a_visualization.py +0 -0
  52. {treequest-0.1.2 → treequest-0.2.0}/tests/test_ab_mcts_m_algo.py +0 -0
  53. {treequest-0.1.2 → treequest-0.2.0}/tests/test_ab_mcts_m_algo_advanced.py +0 -0
  54. {treequest-0.1.2 → treequest-0.2.0}/tests/test_best_first_search.py +0 -0
  55. {treequest-0.1.2 → treequest-0.2.0}/tests/test_interface.py +0 -0
  56. {treequest-0.1.2 → treequest-0.2.0}/tests/test_multi_armed_bandit_ucb.py +0 -0
  57. {treequest-0.1.2 → treequest-0.2.0}/tests/test_standard_mcts.py +0 -0
  58. {treequest-0.1.2 → treequest-0.2.0}/tests/test_tree.py +0 -0
@@ -8,3 +8,6 @@ wheels/
8
8
 
9
9
  # Virtual environments
10
10
  .venv
11
+
12
+ # test images
13
+ *.png
@@ -1,11 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: treequest
3
- Version: 0.1.2
3
+ Version: 0.2.0
4
4
  Summary: A Tree Search Library with Flexible API for LLM Inference-Time Scaling
5
5
  Author-email: Kou Misaki <kou.misaki@sakana.ai>, Takuya Akiba <takiba@sakana.ai>
6
6
  License-File: LICENSE
7
7
  Requires-Python: >=3.11
8
+ Requires-Dist: joblib>=1.5.2
8
9
  Requires-Dist: scipy>=1.0.0
10
+ Requires-Dist: ulid-py>=1.1.0
9
11
  Provides-Extra: abmcts-m
10
12
  Requires-Dist: jax>=0.5.2; extra == 'abmcts-m'
11
13
  Requires-Dist: numpy>=1.26.4; extra == 'abmcts-m'
@@ -62,6 +64,44 @@ best_state, best_node_score = tq.top_k(search_tree, algo, k=1)[0]
62
64
  print(f"Best state: {best_state}, Score: {best_node_score}")
63
65
  ```
64
66
 
67
+ Alternatively, you can use an ask–tell interface with batched AB-MCTS sampling steps:
68
+ ```python
69
+ import random
70
+ import treequest as tq
71
+
72
+ State = str
73
+
74
+ def generate(parent_state: State | None) -> tuple[State, float]:
75
+ ...
76
+
77
+ generate_fns = {"Action A": generate}
78
+ actions = list(generate_fns.keys())
79
+
80
+ # We use batch_size=5 here
81
+ batch_size = 5
82
+
83
+ # It runs AB-MCTS sampling step with 5 processes in parallel
84
+ algo = tq.ABMCTSM(max_process_workers=batch_size)
85
+ search_tree = algo.init_tree()
86
+
87
+ total_budget = 50
88
+ num_steps = total_budget // batch_size
89
+ for _ in range(num_steps):
90
+ # ask_batch returns a list of `Trial` object, which has action, parent_state and trial_id attrs
91
+ search_tree, trials = algo.ask_batch(search_tree, batch_size, actions)
92
+
93
+ for trial in trials:
94
+ result = generate_fns[trial.action](trial.parent_state)
95
+ # Call tell method with trial_id to update search_tree
96
+ search_tree = algo.tell(search_tree, trial.trial_id, result)
97
+
98
+ best_state, best_node_score = tq.top_k(search_tree, algo, k=1)[0]
99
+ ```
100
+
101
+ In particular for AB-MCTS-M, each `step` call can be slow. If you encounter slow execution, prefer `ask_batch` over `step`.
102
+ Please note that using a large `batch_size` can skew the search-tree shape (i.e., the tree may become too wide), so it is best to avoid overly large `batch_size`, see [PROFILING.md](./docs/PROFILING.md) for example trees.
103
+ We recommend `batch_size<=5` as a starting point.
104
+
65
105
  ## Features
66
106
  - Easy-to-use API with customizable node generation and node scoring logic.
67
107
  - **AB-MCTS-A** and **AB-MCTS-M**, as well as **Multi-LLM AB-MCTS** support (See [our paper](https://arxiv.org/abs/2503.04412) for algorithm details).
@@ -155,6 +195,17 @@ for _ in range(20):
155
195
  ```
156
196
  The variation is not limited to LLM types; you can use different prompts, actions, scoring logic, etc. in `generate_fns`.
157
197
 
198
+ ### Batch Semantics and Concurrency
199
+ - Algorithms are stateless objects; the evolving tree/search state is returned from `init_tree`, `step`, `ask`, and `tell`.
200
+ - `ask_batch(state, batch_size, actions)` returns exactly `batch_size` Trial objects to expand next.
201
+ - Non-queue algorithms (e.g., `ABMCTSM`, `ABMCTSA`, `MultiArmedBanditUCB`) return exactly `batch_size` Trials.
202
+ - Queue-based algorithms (e.g., `StandardMCTS`, `BestFirstSearchAlgo`, `TreeOfThoughtsBFS`) precompute a set of parent/action pairs and duplicate them if needed to fill `batch_size`.
203
+ - `tell(state, trial_id, (new_state, score))` reflects the result for the corresponding Trial.
204
+ - Order-independent: you can call `tell` in any order; reflection is tied to `trial_id`.
205
+ - Idempotent: calling `tell` twice on the same `trial_id` does not add extra nodes.
206
+ - For queue-based algorithms, over-told Trials beyond possible number of childs from a parent node (e.g., `(# actions)*samples_per_action` for StandardMCTS) become `INVALID` and are not reflected.
207
+ - Scores are expected to be normalized to the `[0, 1]` range.
208
+
158
209
  ## Algorithms
159
210
 
160
211
  ### ABMCTS-A: ABMCTS with Node Aggregation
@@ -44,6 +44,44 @@ best_state, best_node_score = tq.top_k(search_tree, algo, k=1)[0]
44
44
  print(f"Best state: {best_state}, Score: {best_node_score}")
45
45
  ```
46
46
 
47
+ Alternatively, you can use an ask–tell interface with batched AB-MCTS sampling steps:
48
+ ```python
49
+ import random
50
+ import treequest as tq
51
+
52
+ State = str
53
+
54
+ def generate(parent_state: State | None) -> tuple[State, float]:
55
+ ...
56
+
57
+ generate_fns = {"Action A": generate}
58
+ actions = list(generate_fns.keys())
59
+
60
+ # We use batch_size=5 here
61
+ batch_size = 5
62
+
63
+ # It runs AB-MCTS sampling step with 5 processes in parallel
64
+ algo = tq.ABMCTSM(max_process_workers=batch_size)
65
+ search_tree = algo.init_tree()
66
+
67
+ total_budget = 50
68
+ num_steps = total_budget // batch_size
69
+ for _ in range(num_steps):
70
+ # ask_batch returns a list of `Trial` object, which has action, parent_state and trial_id attrs
71
+ search_tree, trials = algo.ask_batch(search_tree, batch_size, actions)
72
+
73
+ for trial in trials:
74
+ result = generate_fns[trial.action](trial.parent_state)
75
+ # Call tell method with trial_id to update search_tree
76
+ search_tree = algo.tell(search_tree, trial.trial_id, result)
77
+
78
+ best_state, best_node_score = tq.top_k(search_tree, algo, k=1)[0]
79
+ ```
80
+
81
+ In particular for AB-MCTS-M, each `step` call can be slow. If you encounter slow execution, prefer `ask_batch` over `step`.
82
+ Please note that using a large `batch_size` can skew the search-tree shape (i.e., the tree may become too wide), so it is best to avoid overly large `batch_size`, see [PROFILING.md](./docs/PROFILING.md) for example trees.
83
+ We recommend `batch_size<=5` as a starting point.
84
+
47
85
  ## Features
48
86
  - Easy-to-use API with customizable node generation and node scoring logic.
49
87
  - **AB-MCTS-A** and **AB-MCTS-M**, as well as **Multi-LLM AB-MCTS** support (See [our paper](https://arxiv.org/abs/2503.04412) for algorithm details).
@@ -137,6 +175,17 @@ for _ in range(20):
137
175
  ```
138
176
  The variation is not limited to LLM types; you can use different prompts, actions, scoring logic, etc. in `generate_fns`.
139
177
 
178
+ ### Batch Semantics and Concurrency
179
+ - Algorithms are stateless objects; the evolving tree/search state is returned from `init_tree`, `step`, `ask`, and `tell`.
180
+ - `ask_batch(state, batch_size, actions)` returns exactly `batch_size` Trial objects to expand next.
181
+ - Non-queue algorithms (e.g., `ABMCTSM`, `ABMCTSA`, `MultiArmedBanditUCB`) return exactly `batch_size` Trials.
182
+ - Queue-based algorithms (e.g., `StandardMCTS`, `BestFirstSearchAlgo`, `TreeOfThoughtsBFS`) precompute a set of parent/action pairs and duplicate them if needed to fill `batch_size`.
183
+ - `tell(state, trial_id, (new_state, score))` reflects the result for the corresponding Trial.
184
+ - Order-independent: you can call `tell` in any order; reflection is tied to `trial_id`.
185
+ - Idempotent: calling `tell` twice on the same `trial_id` does not add extra nodes.
186
+ - For queue-based algorithms, over-told Trials beyond possible number of childs from a parent node (e.g., `(# actions)*samples_per_action` for StandardMCTS) become `INVALID` and are not reflected.
187
+ - Scores are expected to be normalized to the `[0, 1]` range.
188
+
140
189
  ## Algorithms
141
190
 
142
191
  ### ABMCTS-A: ABMCTS with Node Aggregation
@@ -0,0 +1,98 @@
1
+ # Performance Investigation of `ask_batch` method
2
+ Here, we report the profiling result of `ask_batch` with different `batch_size`. As we increase `batch_size`, AB-MCTS runs are accelerated, while the search tree shapes get more wide.
3
+
4
+ I profiled TreeQuest on a 32-core x86-64 machine running Ubuntu 20.04. As for AB-MCTS-M, it runs 4 parallel MCMC chains per process, so I tested `batch_size` from 1 to 10.
5
+
6
+ ## Profiling Commands
7
+
8
+ ### AB-MCTS-M
9
+ ```bash
10
+ hyperfine \
11
+ 'uv run tests/profiling/ab_mcts_m.py -b 1' \
12
+ 'uv run tests/profiling/ab_mcts_m.py -b 2' \
13
+ 'uv run tests/profiling/ab_mcts_m.py -b 5' \
14
+ 'uv run tests/profiling/ab_mcts_m.py -b 10' \
15
+ -w 0 -r 3 --export-markdown tests/profiling/benchmark_m.md
16
+ ```
17
+
18
+ ### AB-MCTS-A
19
+ ```bash
20
+ hyperfine \
21
+ 'uv run tests/profiling/ab_mcts_a.py -b 1' \
22
+ 'uv run tests/profiling/ab_mcts_a.py -b 2' \
23
+ 'uv run tests/profiling/ab_mcts_a.py -b 5' \
24
+ 'uv run tests/profiling/ab_mcts_a.py -b 10' \
25
+ -w 0 -r 3 --export-markdown tests/profiling/benchmark_a.md
26
+ ```
27
+
28
+ ## Analysis for various `batch_size`
29
+ ### AB-MCTS-M
30
+ #### Time Profiling
31
+ For a search tree with (# non-root nodes) = 50,
32
+ | Command | Mean [s] | Min [s] | Max [s] | Relative |
33
+ |:---|---:|---:|---:|---:|
34
+ | `uv run tests/profiling/ab_mcts_m.py -b 1` | 372.156 ± 37.808 | 342.916 | 414.852 | 6.98 ± 0.72 |
35
+ | `uv run tests/profiling/ab_mcts_m.py -b 2` | 202.324 ± 8.006 | 194.178 | 210.182 | 3.80 ± 0.16 |
36
+ | `uv run tests/profiling/ab_mcts_m.py -b 5` | 91.418 ± 2.391 | 90.026 | 94.179 | 1.71 ± 0.05 |
37
+ | `uv run tests/profiling/ab_mcts_m.py -b 10` | 53.307 ± 0.747 | 52.468 | 53.902 | 1.00 |
38
+
39
+ > ![NOTE]
40
+ > For more resource-constrained systems, we get more mild boost. I ran profiling for M3 MacBook Air with 4 Pcores and 4 Ecores:
41
+ > | Command | Mean [s] | Min [s] | Max [s] | Relative |
42
+ > |:---|---:|---:|---:|---:|
43
+ > | `uv run tests/profiling/ab_mcts_m.py -b 1` | 147.564 ± 4.527 | 144.297 | 152.731 | 1.90 ± 0.08 |
44
+ > | `uv run tests/profiling/ab_mcts_m.py -b 2` | 116.557 ± 3.015 | 113.391 | 119.395 | 1.50 ± 0.06 |
45
+ > | `uv run tests/profiling/ab_mcts_m.py -b 5` | 90.628 ± 1.760 | 89.299 | 92.624 | 1.17 ± 0.04 |
46
+ > | `uv run tests/profiling/ab_mcts_m.py -b 10` | 77.773 ± 2.050 | 75.819 | 79.908 | 1.00 |
47
+
48
+
49
+ We note that different batch size leads to different tree shape; The larger batch size generates wider search tree:
50
+
51
+ #### `batch_size=1`
52
+ ![batch_size=1](../images/batch/abmcts_m_batch_size_1.jpg)
53
+
54
+ #### `batch_size=2`
55
+ ![batch_size=2](../images/batch/abmcts_m_batch_size_2.jpg)
56
+
57
+ #### `batch_size=5`
58
+ ![batch_size=5](../images/batch/abmcts_m_batch_size_5.jpg)
59
+
60
+ #### `batch_size=10`
61
+ ![batch_size=10](../images/batch/abmcts_m_batch_size_10.jpg)
62
+
63
+ ## Analysis for various `batch_size`
64
+ ### AB-MCTS-A
65
+ #### Time Profiling
66
+ AB-MCTS-A is faster than AB-MCTS-M, so I tested (# non-root nodes) = 1000:
67
+ | Command | Mean [s] | Min [s] | Max [s] | Relative |
68
+ |:---|---:|---:|---:|---:|
69
+ | `uv run tests/profiling/ab_mcts_a.py -b 1` | 26.116 ± 0.656 | 25.677 | 26.871 | 1.00 |
70
+ | `uv run tests/profiling/ab_mcts_a.py -b 2` | 26.191 ± 0.709 | 25.599 | 26.976 | 1.00 ± 0.04 |
71
+ | `uv run tests/profiling/ab_mcts_a.py -b 5` | 26.404 ± 0.441 | 26.061 | 26.902 | 1.01 ± 0.03 |
72
+ | `uv run tests/profiling/ab_mcts_a.py -b 10` | 26.399 ± 0.364 | 25.998 | 26.709 | 1.01 ± 0.03 |
73
+
74
+
75
+ > [!NOTE]
76
+ > I didn't parallelize AB-MCTS-A, so the command running time is the same for different batch sizes.
77
+ > Intially I also implemented parallel run for AB-MCTS-A as well, but it didn't lead to much speedup:
78
+ > | Command | Mean [s] | Min [s] | Max [s] | Relative |
79
+ > |:---|---:|---:|---:|---:|
80
+ > | `uv run tests/profiling/ab_mcts_a.py -b 1` | 49.973 ± 2.909 | 47.645 | 53.235 | 2.79 ± 0.16 |
81
+ > | `uv run tests/profiling/ab_mcts_a.py -b 2` | 30.971 ± 0.546 | 30.528 | 31.581 | 1.73 ± 0.03 |
82
+ > | `uv run tests/profiling/ab_mcts_a.py -b 5` | 21.256 ± 0.706 | 20.732 | 22.058 | 1.18 ± 0.04 |
83
+ > | `uv run tests/profiling/ab_mcts_a.py -b 10` | 17.941 ± 0.110 | 17.867 | 18.068 | 1.00 |
84
+
85
+
86
+ We note that different batch size leads to different tree shape; The larger batch size generates wider search tree:
87
+
88
+ #### `batch_size=1`
89
+ ![batch_size=1](../images/batch/abmcts_m_batch_size_1.jpg)
90
+
91
+ #### `batch_size=2`
92
+ ![batch_size=2](../images/batch/abmcts_m_batch_size_2.jpg)
93
+
94
+ #### `batch_size=5`
95
+ ![batch_size=5](../images/batch/abmcts_m_batch_size_5.jpg)
96
+
97
+ #### `batch_size=10`
98
+ ![batch_size=10](../images/batch/abmcts_m_batch_size_10.jpg)
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "treequest"
3
- version = "0.1.2"
3
+ version = "0.2.0"
4
4
  description = "A Tree Search Library with Flexible API for LLM Inference-Time Scaling"
5
5
  readme = "README.md"
6
6
  authors = [
@@ -9,7 +9,9 @@ authors = [
9
9
  ]
10
10
  requires-python = ">=3.11"
11
11
  dependencies = [
12
+ "joblib>=1.5.2",
12
13
  "scipy>=1.0.0",
14
+ "ulid-py>=1.1.0",
13
15
  ]
14
16
 
15
17
  [project.optional-dependencies]
@@ -1,8 +1,6 @@
1
1
  from .algos.ab_mcts_m._ab_mcts_m_imports import _import as _ab_mcts_m_import
2
2
 
3
- if _ab_mcts_m_import.is_successful():
4
- from .algos.ab_mcts_m.algo import ABMCTSM
5
- else:
3
+ if not _ab_mcts_m_import.is_successful():
6
4
  # Create a placeholder that raises an informative error when accessed
7
5
  class _ABMCTSMPlaceholder:
8
6
  def __getattr__(self, name): # type: ignore
@@ -14,6 +12,8 @@ else:
14
12
  raise ImportError("ABMCTSM import failed.")
15
13
 
16
14
  ABMCTSM = _ABMCTSMPlaceholder() # type: ignore
15
+ else:
16
+ from .algos.ab_mcts_m.algo import ABMCTSM # type: ignore[assignment]
17
17
 
18
18
  from .algos.ab_mcts_a.algo import ABMCTSA
19
19
  from .algos.base import Algorithm
@@ -8,6 +8,7 @@ from typing import Dict, Generic, List, Literal, Optional, Tuple, TypeVar, Union
8
8
  from treequest.algos.ab_mcts_a.prob_state import NodeProbState, PriorConfig
9
9
  from treequest.algos.base import Algorithm
10
10
  from treequest.algos.tree import Node, Tree
11
+ from treequest.trial import Trial, TrialId, TrialStore
11
12
  from treequest.types import GenerateFnType, StateScoreType
12
13
 
13
14
  # Type variable for state
@@ -20,7 +21,7 @@ logger = getLogger(__name__)
20
21
  class ABMCTSAStateManager(Generic[StateT]):
21
22
  """Manager for ABMCTSAState instances associated with expand_idx values."""
22
23
 
23
- states: Dict[int, NodeProbState] = field(default_factory=dict)
24
+ states: Dict[int, NodeProbState] = field(default_factory=dict[int, NodeProbState])
24
25
  default_prior_config: Optional["PriorConfig"] = None
25
26
  default_reward_average_priors: Optional[Union[float, Dict[str, float]]] = None
26
27
  default_model_selection_strategy: str = "multiarm_bandit_thompson"
@@ -32,13 +33,13 @@ class ABMCTSAStateManager(Generic[StateT]):
32
33
  def __len__(self) -> int:
33
34
  return len(self.states)
34
35
 
35
- def get(self, node: Node) -> Optional[NodeProbState]:
36
+ def get(self, node: Node[StateT]) -> Optional[NodeProbState]:
36
37
  """Get thompson state for the given expand_idx if it exists."""
37
38
  return self.states.get(node.expand_idx)
38
39
 
39
40
  def create(
40
41
  self,
41
- node: Node,
42
+ node: Node[StateT],
42
43
  actions: List[str],
43
44
  prior_config: Optional["PriorConfig"] = None,
44
45
  reward_average_priors: Optional[Union[float, Dict[str, float]]] = None,
@@ -65,7 +66,7 @@ class ABMCTSAStateManager(Generic[StateT]):
65
66
 
66
67
  def get_or_create(
67
68
  self,
68
- node: Node,
69
+ node: Node[StateT],
69
70
  actions: List[str],
70
71
  prior_config: Optional["PriorConfig"] = None,
71
72
  reward_average_priors: Optional[Union[float, Dict[str, float]]] = None,
@@ -96,10 +97,13 @@ class ABMCTSAAlgoState(Generic[StateT]):
96
97
  """State for ABMCTSA algorithm."""
97
98
 
98
99
  tree: Tree[StateT]
99
- thompson_states: ABMCTSAStateManager = field(default_factory=ABMCTSAStateManager)
100
+ thompson_states: ABMCTSAStateManager[StateT] = field(
101
+ default_factory=ABMCTSAStateManager[StateT]
102
+ )
100
103
  all_rewards_store: Dict[str, List[float]] = field(
101
104
  default_factory=build_default_dict_of_list
102
105
  )
106
+ trial_store: TrialStore[StateT] = field(default_factory=TrialStore[StateT])
103
107
 
104
108
 
105
109
  class ABMCTSA(Algorithm[StateT, ABMCTSAAlgoState[StateT]]):
@@ -155,14 +159,14 @@ class ABMCTSA(Algorithm[StateT, ABMCTSAAlgoState[StateT]]):
155
159
  )
156
160
  self.model_selection_strategy = model_selection_strategy
157
161
 
158
- def init_tree(self) -> ABMCTSAAlgoState:
162
+ def init_tree(self) -> ABMCTSAAlgoState[StateT]:
159
163
  """
160
164
  Initialize the algorithm state with an empty tree.
161
165
 
162
166
  Returns:
163
167
  Initial algorithm state
164
168
  """
165
- tree: Tree = Tree.with_root_node()
169
+ tree: Tree[StateT] = Tree.with_root_node()
166
170
  state_manager = ABMCTSAStateManager[StateT](
167
171
  default_prior_config=self.prior_config,
168
172
  default_reward_average_priors=self.reward_average_priors,
@@ -172,55 +176,56 @@ class ABMCTSA(Algorithm[StateT, ABMCTSAAlgoState[StateT]]):
172
176
 
173
177
  def step(
174
178
  self,
175
- state: ABMCTSAAlgoState,
179
+ state: ABMCTSAAlgoState[StateT],
176
180
  generate_fn: Mapping[str, GenerateFnType[StateT]],
177
181
  inplace: bool = False,
178
- ) -> ABMCTSAAlgoState:
182
+ ) -> ABMCTSAAlgoState[StateT]:
179
183
  """
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
184
+ Perform one step of the AB-MCTS-A algorithm and generate a new node.
188
185
  """
189
186
  if not inplace:
190
187
  state = copy.deepcopy(state)
191
188
 
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] = []
189
+ actions = list(generate_fn.keys())
190
+ state, trial = self.ask(state, actions)
196
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]:
197
204
  # If the tree is empty (only root), expand the root
198
205
  if not state.tree.root.children:
199
- self._expand_node(state, state.tree.root, generate_fn)
200
- return state
206
+ return state.tree.root, self._get_generation_action(
207
+ state, state.tree.root, actions
208
+ )
201
209
 
202
210
  # Run one simulation step
203
211
  node = state.tree.root
204
212
 
205
213
  # Selection phase: traverse tree until we reach a leaf node or need to create a new node
206
214
  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
215
+ node, action = self._select_child(state, node, actions)
212
216
 
213
- # Expansion phase: expand leaf node
214
- self._expand_node(state, node, generate_fn)
215
-
216
- return state
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
217
222
 
218
223
  def _select_child(
219
224
  self,
220
- state: ABMCTSAAlgoState,
221
- node: Node,
222
- generate_fn: Mapping[str, GenerateFnType[StateT]],
223
- ) -> Tuple[Node, Optional[str]]:
225
+ state: ABMCTSAAlgoState[StateT],
226
+ node: Node[StateT],
227
+ actions: list[str],
228
+ ) -> Tuple[Node[StateT], Optional[str]]:
224
229
  """
225
230
  Select a child node using Thompson Sampling.
226
231
 
@@ -235,7 +240,7 @@ class ABMCTSA(Algorithm[StateT, ABMCTSAAlgoState[StateT]]):
235
240
  # Get or create thompson state for this node
236
241
  thompson_state = state.thompson_states.get_or_create(
237
242
  node,
238
- list(generate_fn.keys()),
243
+ actions,
239
244
  )
240
245
 
241
246
  # Ask for next node or action using Thompson Sampling
@@ -243,8 +248,7 @@ class ABMCTSA(Algorithm[StateT, ABMCTSAAlgoState[StateT]]):
243
248
 
244
249
  # If string returned, we need to generate a new node with that action
245
250
  if isinstance(selection, str):
246
- new_node = self._generate_new_child(state, node, generate_fn, selection)
247
- return new_node, selection
251
+ return node, selection
248
252
  else:
249
253
  # Otherwise, we return the existing child with that index
250
254
  if selection >= len(node.children):
@@ -253,27 +257,13 @@ class ABMCTSA(Algorithm[StateT, ABMCTSAAlgoState[StateT]]):
253
257
  )
254
258
  return node.children[selection], None
255
259
 
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
- """
260
+ def _get_generation_action(
261
+ self, state: ABMCTSAAlgoState[StateT], node: Node[StateT], actions: list[str]
262
+ ) -> str:
273
263
  # Create thompson state for this node if it doesn't exist
274
264
  thompson_state = state.thompson_states.get_or_create(
275
265
  node,
276
- list(generate_fn.keys()),
266
+ actions,
277
267
  )
278
268
 
279
269
  # Get action to use for generating child
@@ -285,53 +275,14 @@ class ABMCTSA(Algorithm[StateT, ABMCTSAAlgoState[StateT]]):
285
275
  f"Something went wrong in ABMCTSA algorithm: selection should always be str when the expansion is from the leaf node, whle got {selection}"
286
276
  )
287
277
 
288
- new_node = self._generate_new_child(state, node, generate_fn, selection)
289
- return new_node, selection
278
+ return selection
290
279
 
291
- def _generate_new_child(
280
+ def _backpropagate(
292
281
  self,
293
- state: ABMCTSAAlgoState,
294
- node: Node,
295
- generate_fn: Mapping[str, GenerateFnType[StateT]],
282
+ state: ABMCTSAAlgoState[StateT],
283
+ node: Node[StateT],
284
+ score: float,
296
285
  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
286
  ) -> None:
336
287
  """
337
288
  Update Thompson Sampling statistics for all nodes in the path from node to root.
@@ -369,7 +320,7 @@ class ABMCTSA(Algorithm[StateT, ABMCTSAAlgoState[StateT]]):
369
320
  current = current.parent
370
321
 
371
322
  def get_state_score_pairs(
372
- self, state: ABMCTSAAlgoState
323
+ self, state: ABMCTSAAlgoState[StateT]
373
324
  ) -> List[StateScoreType[StateT]]:
374
325
  """
375
326
  Get all the state-score pairs from the tree.
@@ -381,3 +332,57 @@ class ABMCTSA(Algorithm[StateT, ABMCTSAAlgoState[StateT]]):
381
332
  List of (state, score) pairs
382
333
  """
383
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(result, parent_node)
373
+
374
+ # Update Thompson state with the new node
375
+ thompson_state = state.thompson_states.get(parent_node)
376
+ if thompson_state:
377
+ thompson_state.register_new_child_node(
378
+ action, new_node, self.model_selection_strategy
379
+ )
380
+ else:
381
+ raise RuntimeError(
382
+ f"Internal Error in ABMCTSA: thompson_state should not be None for node {parent_node}"
383
+ )
384
+
385
+ # Backpropagate the score through the parents
386
+ self._backpropagate(state, new_node, new_score, action)
387
+
388
+ return state