Solver
Configure and run the solver — phases, moves, termination, and SolverManager.
The solver takes your domain model and constraints, then searches for the best solution using metaheuristic algorithms. Configuration controls which algorithms run, how long to search, and how moves are selected.
Quick Start
use solverforge::prelude::*;
static MANAGER: SolverManager<Schedule> = SolverManager::new();
let config = SolverConfig::from_toml_str(r#"
[termination]
seconds_spent_limit = 30
"#).unwrap();
let (job_id, rx) = MANAGER.solve(problem);
// Receive improving solutions via channel
for (solution, score) in rx {
println!("New best score: {:?}", score);
}
Sections
1 - Configuration
TOML-based solver configuration with SolverConfig.
SolverForge uses TOML for solver configuration. You can load configuration from a file or build it from a string.
Loading Configuration
From a TOML file
let config = SolverConfig::load("solver-config.toml").unwrap();
From a TOML string
let config = SolverConfig::from_toml_str(r#"
environment_mode = "reproducible"
move_thread_count = 4
[termination]
seconds_spent_limit = 120
[[phases]]
type = "construction_heuristic"
construction_heuristic_type = "first_fit"
[[phases]]
type = "local_search"
[phases.acceptor]
type = "late_acceptance"
late_acceptance_size = 400
"#).unwrap();
Example TOML File
environment_mode = "non_reproducible"
move_thread_count = "auto"
[termination]
seconds_spent_limit = 300
unimproved_seconds_spent_limit = 60
[[phases]]
type = "construction_heuristic"
construction_heuristic_type = "first_fit"
[[phases]]
type = "local_search"
[phases.acceptor]
type = "simulated_annealing"
starting_temperature = "0hard/500soft"
[phases.termination]
step_count_limit = 100000
Configuration Options
Environment Mode
Controls reproducibility and assertion levels.
| Mode | Description |
|---|
reproducible | Deterministic — same input always produces the same output. Slower. |
non_reproducible | Non-deterministic — fastest mode for production use |
fast_assert | Enables light assertions for debugging |
full_assert | Enables all assertions — slowest, for development only |
environment_mode = "non_reproducible"
Move Thread Count
Controls multi-threaded move evaluation.
move_thread_count = 4 # Fixed thread count
move_thread_count = "auto" # Use available cores
move_thread_count = "none" # Single-threaded (default)
Phases
Phases run in sequence. A typical configuration uses construction heuristic followed by local search:
[[phases]]
type = "construction_heuristic"
construction_heuristic_type = "first_fit"
[[phases]]
type = "local_search"
[phases.acceptor]
type = "tabu_search"
entity_tabu_size = 7
See Phases for all phase types and their options.
Termination
Controls when the solver stops. See Termination for all options.
[termination]
seconds_spent_limit = 300
See Also
2 - Solver Phases
Construction heuristic, local search, exhaustive search, partitioned search, and VND.
The solver runs phases in sequence. Each phase uses a different strategy to improve the solution.
Construction Heuristic
Builds an initial solution by assigning values to all planning variables. Runs first — local search then improves the result.
Construction Heuristic Types
| Type | Description |
|---|
first_fit | Assigns the first feasible value found. Fast. |
first_fit_decreasing | First fit, processing entities by difficulty. |
weakest_fit | Assigns the value that leaves the most room for future assignments. |
weakest_fit_decreasing | Weakest fit, processing entities by difficulty. |
strongest_fit | Assigns the value that uses resources most aggressively. |
strongest_fit_decreasing | Strongest fit, processing entities by difficulty. |
cheapest_insertion | Greedy insertion for basic variables. |
list_round_robin | Distributes elements evenly across entities (list variables). |
list_cheapest_insertion | Inserts each element at the score-minimizing position (list variables). |
list_regret_insertion | Inserts elements in order of highest placement regret (list variables). |
list_clarke_wright | Greedy route merging by savings value (list variables). |
list_k_opt | Per-route k-opt polishing (list variables). |
[[phases]]
type = "construction_heuristic"
construction_heuristic_type = "first_fit"
Local Search
Iteratively improves the solution by applying moves and accepting improvements (and sometimes worse moves to escape local optima).
Acceptors
Local search uses an acceptor to decide whether to keep a move. The acceptor is configured as a nested object:
| Acceptor | Description |
|---|
hill_climbing | Only accepts improving moves. Fast but gets stuck in local optima. |
simulated_annealing | Accepts worse moves with decreasing probability. Good exploration. |
tabu_search | Remembers recent moves and forbids reversing them. Strong for many problems. |
late_acceptance | Accepts moves better than N steps ago. Simple and effective. |
great_deluge | Accepts moves above a rising water level. Steady improvement. |
[[phases]]
type = "local_search"
[phases.acceptor]
type = "late_acceptance"
late_acceptance_size = 400
[phases.termination]
unimproved_step_count_limit = 10000
Acceptor-Specific Configuration
Simulated Annealing:
[phases.acceptor]
type = "simulated_annealing"
starting_temperature = "0hard/500soft"
Tabu Search:
[phases.acceptor]
type = "tabu_search"
entity_tabu_size = 7
# or value_tabu_size, move_tabu_size
Late Acceptance:
[phases.acceptor]
type = "late_acceptance"
late_acceptance_size = 400
Exhaustive Search
Explores the entire search space systematically. Only practical for small problems.
Branch and Bound
[[phases]]
type = "exhaustive_search"
exhaustive_search_type = "branch_and_bound"
| Type | Description |
|---|
branch_and_bound | Prunes branches that can’t improve — memory efficient, finds solutions quickly |
brute_force | Explores every possibility |
Score Bounder
Use a ScoreBounder to prune branches that can’t improve on the best known solution, dramatically reducing the search space.
Partitioned Search
Splits the problem into independent partitions and solves them in parallel on separate threads.
[[phases]]
type = "partitioned_search"
Requires implementing the SolutionPartitioner trait to define how the problem is split.
Typical Phase Configuration
Most problems work well with construction heuristic + local search:
[[phases]]
type = "construction_heuristic"
construction_heuristic_type = "first_fit"
[[phases]]
type = "local_search"
[phases.acceptor]
type = "late_acceptance"
late_acceptance_size = 400
For better results, try tabu search or simulated annealing as the acceptor.
See Also
3 - Moves
Move types, selectors, and the zero-allocation MoveArena.
Moves are the atomic operations the solver uses to explore the search space. Each move modifies one or more planning variables and the solver evaluates whether the change improves the score.
Basic Moves
| Move | Description |
|---|
ChangeMove | Changes one planning variable to a different value |
SwapMove | Swaps planning variable values between two entities |
CompositeMove | Applies multiple moves together as one atomic step |
List Moves
For list variables:
| Move | Description |
|---|
ListChangeMove | Moves an element to a different position or list |
ListSwapMove | Swaps two elements between or within lists |
ListReverseMove | Reverses a subsequence within a list (2-opt) |
SubListChangeMove | Moves a contiguous subsequence to another position |
SubListSwapMove | Swaps two contiguous subsequences |
Advanced Moves
| Move | Description |
|---|
KOptMove | K-opt style moves — removes K edges and reconnects. Powerful for routing. |
RuinMove | Removes a set of elements and reinserts them (ruin-and-recreate). Escapes deep local optima. |
PillarChangeMove | Changes the same variable on a group of related entities simultaneously |
PillarSwapMove | Swaps variable values between two groups of related entities |
MoveArena (Zero-Allocation)
SolverForge uses a MoveArena for move storage — all moves are allocated in an arena that is cleared in O(1) at the end of each step. This eliminates per-move heap allocations and GC pressure.
Moves are stored inline without boxing:
ChangeMove<S, V> and SwapMove<S, V> are concrete generic types- No dynamic dispatch or trait objects for move evaluation
- Arena allocation provides O(1) per-step cleanup
Selectors
Selectors control which moves are generated and in what order.
Entity Selector
Controls which entities are considered for moves.
Value Selector
Controls which values are tried for assignments.
Move Selector
Controls which moves are generated from selected entities and values.
Nearby Selection
For large problems, nearby selection restricts move generation to entities/values that are “close” to each other according to a distance measure. This focuses the search on promising neighborhoods.
[[phases]]
type = "local_search"
nearby_selection = true
nearby_distance_type = "euclidean"
Pillar Selector
Groups related entities (e.g., all shifts for the same employee) for pillar moves.
Mimic Selector
Coordinates multiple selectors to use the same selection, ensuring move components are aligned.
See Also
4 - Termination
Control when the solver stops — time limits, step counts, score targets, and composites.
Termination conditions tell the solver when to stop searching. You can set termination globally or per-phase.
Termination Types
Time-Based
[termination]
seconds_spent_limit = 300 # Stop after 5 minutes
minutes_spent_limit = 10 # Stop after 10 minutes
Step-Based
[termination]
step_count_limit = 100000 # Stop after 100k steps
Unimproved Time/Steps
Stop when no improvement has been found for a duration:
[termination]
unimproved_seconds_spent_limit = 60 # No improvement for 60s
unimproved_step_count_limit = 10000 # No improvement for 10k steps
Score-Based
Stop when a target score is reached:
[termination]
best_score_limit = "0hard/-100soft" # Stop when feasible with soft ≥ -100
Composite Termination
Combine multiple conditions — by default, the solver stops when any condition is met:
[termination]
seconds_spent_limit = 300
best_score_limit = "0hard/0soft"
# Stops after 5 minutes OR when a perfect score is found
Per-Phase Termination
Each phase can have its own termination:
[[phases]]
type = "construction_heuristic"
# No termination needed — runs until all variables are assigned
[[phases]]
type = "local_search"
[phases.acceptor]
type = "late_acceptance"
late_acceptance_size = 400
[phases.termination]
unimproved_step_count_limit = 10000
Programmatic Termination
Use SolverManager::terminate_early(job_id) to stop a job from code:
static MANAGER: SolverManager<Schedule> = SolverManager::new();
let (job_id, rx) = MANAGER.solve(problem);
// Later...
MANAGER.terminate_early(job_id);
See Also
5 - SolverManager
Run and manage solver instances with channel-based streaming.
SolverManager is the main entry point for running the solver. It manages solver lifecycle, provides streaming updates via channels, and supports early termination.
Creating a SolverManager
SolverManager::new() is a const fn that takes no arguments. It’s designed to be used as a static:
use solverforge::prelude::*;
static MANAGER: SolverManager<Schedule> = SolverManager::new();
Solving
Call .solve() with your planning solution. It returns a (job_id, Receiver) tuple:
let (job_id, rx) = MANAGER.solve(solution);
The receiver yields (S, S::Score) tuples — each is an improving solution found during solving. Consume them in a loop or spawn a thread:
let (job_id, rx) = MANAGER.solve(solution);
for (best_solution, score) in rx {
println!("New best score: {:?}", score);
// Update UI, save to database, etc.
}
Solver Status
Check the current state of a job:
let status = MANAGER.get_status(job_id);
match status {
SolverStatus::NotSolving => println!("Not solving"),
SolverStatus::Solving => println!("Currently solving"),
}
The two variants are:
SolverStatus::NotSolving — the job is idle or finishedSolverStatus::Solving — the job is actively running
Early Termination
Stop a job before its configured termination condition:
let terminated = MANAGER.terminate_early(job_id);
// Returns true if the job was found and was currently solving
The solver finishes its current step and sends the best solution found so far through the channel.
Freeing Slots
After a job completes and you’ve consumed the results, free the slot:
MANAGER.free_slot(job_id);
Active Jobs
Check how many jobs are currently running:
let count = MANAGER.active_job_count();
The Solvable Trait
Your planning solution must implement the Solvable trait (derived automatically by the #[planning_solution] macro) to be used with SolverManager.
See Also