Files
LLMMCTS/src/interface.jl
T
2026-07-04 12:48:16 +07:00

330 lines
10 KiB
Julia

module interface
export runMCTS
using Base.Threads, PrettyPrinting
using ..type, ..mcts, ..util
# ---------------------------------------------- 100 --------------------------------------------- #
""" Search for the best action to take for a given state and task.
This function runs the MCTS algorithm through multiple iterations of expansion,
simulation, and backpropagation to find optimal decisions.
Does **not** mutate the input state; it creates new MCTS nodes during search.
# Arguments
- `initialstate::T`
The initial state from which to start the search.
- `transition::Function`
A function that defines how the state transitions.
- `transitionargs::NamedTuple`
Arguments passed to the transition function.
# Keyword Arguments
- `horizontalSampleExpansionPhase::Integer=3`
Number of child states sampled at each node during expansion phase.
- `horizontalSampleSimulationPhase::Integer=3`
Number of child states sampled at each node during simulation's expansion phase.
- `maxSimulationDepth::Integer=3`
Maximum depth MCTS goes during simulation phase.
- `maxiterations::Integer=10`
Number of iterations MCTS performs through expansion → simulation → backpropagation cycles.
- `explorationweight::Number=1.0`
Exploration weight controls how much MCTS explores new states versus exploiting known states.
A value of 1.0 balances exploration and exploitation equally. Higher values (e.g., 2.0)
encourage more aggressive exploration.
- `earlystop::Union{Function,Nothing}=nothing`
Optional function to check early stopping condition. If satisfied, MCTS breaks iterations.
- `saveSimulatedNode::Bool=false`
Whether to save nodes created during simulation phase.
- `multithread::Bool=false`
Whether to use multithreading during simulation.
# Return
- `NamedTuple{(:root, :bestNextState, :bestTerminalState, :highValueStateList),
Tuple{MCTSNode,T,T,Vector{Dict{String,Any}}}}`
- `root`: the complete MCTS tree with root node
- `bestNextState`: the best immediate next state
- `bestTerminalState`: the best final state along the best trajectory
- `highValueStateList`: list of high-value terminal states (reward >= 8)
# Example
```jldoctest
julia> using LLMMCTS
julia> initialState = Dict(:reward=>0.0)
julia> result = runMCTS(initialState, transition_func, transition_args; maxiterations=5)
```
"""
function runMCTS(
initialstate::T,
transition::Function,
transitionargs::NamedTuple,
;
horizontalSampleExpansionPhase::Integer=3,
horizontalSampleSimulationPhase::Integer=3,
maxSimulationDepth::Integer=3,
maxiterations::Integer=10,
explorationweight::Number=1.0,
earlystop::Union{Function,Nothing}=nothing,
saveSimulatedNode::Bool=false,
multithread=false,
)::NamedTuple{(:root, :bestNextState, :bestTerminalState, :highValueStateList),
Tuple{MCTSNode,T,T,Vector{Dict{String,Any}}}} where {T<:Any}
println("--> LLMMCTS runMCTS 1")
# Initialize the MCTS tree with a root node representing the initial state
# root.visits=0: no visits yet
# root.statevalue=0: no simulation results yet
root = MCTSNode("root", initialstate, 0, 0, 0, 0, false, nothing, Dict{String,MCTSNode}(),
Dict{String,Any}())
# Channel to collect high-value terminal states (reward >= 8)
# These are "good solutions" that can be returned to the user
highValueState = Channel{Any}(100)
# Main MCTS loop: perform iterations to build the search tree
# Each iteration: SELECTION → EXPANSION → SIMULATION → BACKPROPAGATION
for nth in 1:maxiterations
# Start from root and traverse down using UCT selection
node = root
node.visits += 1 # Count this iteration's visit to root
println("--> LLMMCTS runMCTS 2")
# Phase 1: SELECTION - Traverse tree using UCT until reaching a leaf node
# UCT balances exploration (new branches) vs exploitation (promising branches)
while !isleaf(node)
println("--> LLMMCTS runMCTS 3")
node = UCTselect(node, explorationweight)
end
println("--> LLMMCTS runMCTS 4")
# Phase 2: TERMINAL CHECK - If leaf is terminal, just backpropagate
if node.isterminal
println("--> LLMMCTS runMCTS 5")
# If this terminal state has high reward (>= 8), store it for later
if node.state[:reward] >= 8
println("--> LLMMCTS runMCTS 6")
put!(highValueState, deepcopy(node.state))
end
println("--> LLMMCTS runMCTS 7")
# Backpropagate the terminal node's own reward up to root
# This updates all ancestors with this path's outcome
backpropagate(node, node.reward)
else
println("--> LLMMCTS runMCTS 8")
# Phase 3: EXPANSION - Generate children for this non-terminal leaf
# Horizontal sampling: create multiple child nodes via LLM transition
_ = expand(node, transition, transitionargs;
horizontalSample=horizontalSampleExpansionPhase,
multithread=multithread)
println("--> LLMMCTS runMCTS 9")
# Phase 4: SIMULATION + BACKPROPAGATION
# For each newly expanded child, run simulation and update statistics
if multithread
println("--> LLMMCTS runMCTS 10")
# Parallel simulation: spawn threads for each child node
@sync for (leafNodeKey, leafNode) in node.children
@spawn simulateThenBackpropagate(leafNode, transition, transitionargs;
maxSimulationDepth=maxSimulationDepth,
horizontalSampleSimulationPhase=horizontalSampleSimulationPhase,
saveSimulatedNode=saveSimulatedNode,
multithread=multithread,
highValueState=highValueState,
)
end
else
println("--> LLMMCTS runMCTS 11")
# Sequential simulation: process each child one at a time
for (leafNodeKey, leafNode) in node.children
println("--> LLMMCTS runMCTS 11-1")
simulateThenBackpropagate(leafNode, transition, transitionargs;
maxSimulationDepth=maxSimulationDepth,
horizontalSampleSimulationPhase=horizontalSampleSimulationPhase,
saveSimulatedNode=saveSimulatedNode,
multithread=multithread,
highValueState=highValueState)
end
end
end
println("--> LLMMCTS runMCTS 12")
# Phase 5: EARLY STOP CHECK
# Optional: stop search early if a condition is met
if typeof(earlystop) <: Function && earlystop(node.state)
println("--> LLMMCTS runMCTS 13")
break
end
end
println("--> LLMMCTS runMCTS 14")
# After all iterations, extract results from the search tree
# Select best immediate next state (best child of root)
bestNextState = selectBestNextNode(root)
println("--> LLMMCTS runMCTS 15")
# Select best terminal state along the optimal trajectory
bestTerminalState = selectBestTrajectoryNode(root)
# Collect all high-value states from the channel into a list
highValueStateList = Vector{Dict{String, Any}}()
while !isempty(highValueState)
println("--> LLMMCTS runMCTS 16")
push!(highValueStateList, take!(highValueState))
end
println("--> LLMMCTS runMCTS 17")
# Return complete search results
result = (
root=root,
bestNextState=bestNextState.state,
bestTerminalState=bestTerminalState.state,
highValueStateList=highValueStateList
)
return result
end
""" Run simulation from a given node and backpropagate the reward.
This function performs simulation (rollout) from the given node, collects the
cumulative reward along the trajectory, and backpropagates it up the tree to update
visit counts and state values.
Does **not** mutate the input node's children (unless `saveSimulatedNode=true`).
# Arguments
- `node::MCTSNode`
The current node to simulate from.
- `transition::Function`
A function that defines how the state transitions.
- `transitionargs::NamedTuple`
Arguments passed to the transition function.
# Keyword Arguments
- `maxSimulationDepth::Integer=3`
Maximum depth MCTS goes during simulation phase.
- `horizontalSampleSimulationPhase::Integer=3`
Number of child states sampled at each node during simulation phase.
- `saveSimulatedNode::Bool=false`
Whether to save nodes created during simulation phase. If false, children are
cleared after simulation.
- `multithread::Bool=false`
Whether to use multithreading during simulation.
# Return
- `Nothing`
# Signature
"""
function simulateThenBackpropagate(node::MCTSNode, transition::Function, transitionargs::NamedTuple;
maxSimulationDepth::Integer=3, horizontalSampleSimulationPhase::Integer=3,
saveSimulatedNode::Bool=false,
multithread=false,
highValueState=Union{Nothing,Any}=nothing)
println("--> LLMMCTS simulateThenBackpropagate 1")
# Phase 1: RUN SIMULATION (rollout)
# Perform a rollout from this node, accumulating rewards along the way
simTrajectoryReward, terminalstate =
simulate(node, transition, transitionargs;
maxSimulationDepth=maxSimulationDepth,
horizontalSample=horizontalSampleSimulationPhase,
multithread=multithread)
println("--> LLMMCTS simulateThenBackpropagate 2")
# Phase 2: HIGH-VALUE STATE TRACKING
# If we reached a terminal state with high reward (>= 8), store it
# This allows users to access multiple good solutions, not just the best one
if highValueState !== nothing &&
terminalstate !== nothing &&
terminalstate["reward"] >= 8
println("--> LLMMCTS simulateThenBackpropagate 3")
put!(highValueState, deepcopy(terminalstate))
end
println("--> LLMMCTS simulateThenBackpropagate 4")
# Phase 3: BACKPROPAGATE
# Update statistics (visits, statevalue) for all ancestors up to root
# The simulation result is now incorporated into the tree
backpropagate(node, simTrajectoryReward)
println("--> LLMMCTS simulateThenBackpropagate 5")
# Phase 4: MEMORY MANAGEMENT
# Clear children unless user wants to keep them for analysis
# This frees memory for the next iteration while preserving tree structure
if saveSimulatedNode == false
println("--> LLMMCTS simulateThenBackpropagate 6")
node.children = Dict{String, MCTSNode}()
end
println("--> LLMMCTS simulateThenBackpropagate 7")
end
end # module interface