This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

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

  • Configuration — TOML-based solver configuration
  • Phases — Construction heuristic, local search, exhaustive search
  • Moves — Move types and selectors
  • Termination — When to stop solving
  • SolverManager — Running and managing solver instances

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.

ModeDescription
reproducibleDeterministic — same input always produces the same output. Slower.
non_reproducibleNon-deterministic — fastest mode for production use
fast_assertEnables light assertions for debugging
full_assertEnables 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

TypeDescription
first_fitAssigns the first feasible value found. Fast.
first_fit_decreasingFirst fit, processing entities by difficulty.
weakest_fitAssigns the value that leaves the most room for future assignments.
weakest_fit_decreasingWeakest fit, processing entities by difficulty.
strongest_fitAssigns the value that uses resources most aggressively.
strongest_fit_decreasingStrongest fit, processing entities by difficulty.
cheapest_insertionGreedy insertion for basic variables.
list_round_robinDistributes elements evenly across entities (list variables).
list_cheapest_insertionInserts each element at the score-minimizing position (list variables).
list_regret_insertionInserts elements in order of highest placement regret (list variables).
list_clarke_wrightGreedy route merging by savings value (list variables).
list_k_optPer-route k-opt polishing (list variables).
[[phases]]
type = "construction_heuristic"
construction_heuristic_type = "first_fit"

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:

AcceptorDescription
hill_climbingOnly accepts improving moves. Fast but gets stuck in local optima.
simulated_annealingAccepts worse moves with decreasing probability. Good exploration.
tabu_searchRemembers recent moves and forbids reversing them. Strong for many problems.
late_acceptanceAccepts moves better than N steps ago. Simple and effective.
great_delugeAccepts 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

Explores the entire search space systematically. Only practical for small problems.

Branch and Bound

[[phases]]
type = "exhaustive_search"
exhaustive_search_type = "branch_and_bound"
TypeDescription
branch_and_boundPrunes branches that can’t improve — memory efficient, finds solutions quickly
brute_forceExplores every possibility

Score Bounder

Use a ScoreBounder to prune branches that can’t improve on the best known solution, dramatically reducing the search space.

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

MoveDescription
ChangeMoveChanges one planning variable to a different value
SwapMoveSwaps planning variable values between two entities
CompositeMoveApplies multiple moves together as one atomic step

List Moves

For list variables:

MoveDescription
ListChangeMoveMoves an element to a different position or list
ListSwapMoveSwaps two elements between or within lists
ListReverseMoveReverses a subsequence within a list (2-opt)
SubListChangeMoveMoves a contiguous subsequence to another position
SubListSwapMoveSwaps two contiguous subsequences

Advanced Moves

MoveDescription
KOptMoveK-opt style moves — removes K edges and reconnects. Powerful for routing.
RuinMoveRemoves a set of elements and reinserts them (ruin-and-recreate). Escapes deep local optima.
PillarChangeMoveChanges the same variable on a group of related entities simultaneously
PillarSwapMoveSwaps 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 finished
  • SolverStatus::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