Documentation

Solver Phases

Construction heuristic, local search, VND, typed exact search, and partitioned search.

The solver runs phases in sequence. Each phase uses a different strategy to improve the solution.

Omitted Phase Defaults

When phases are omitted, the runtime runs state-aware construction: matching specialized list construction, named grouped FirstFit for required assignment rows followed by grouped CheapestInsertion for optional rows, and single-slot construction for remaining non-assignment-owned scalars. One streaming default local-search phase is eligible only when top-level termination contains an effective parsed limit. Missing termination or an empty termination object leaves omitted phases construction-only; a best_score_limit that does not parse as the model’s score fails the solve. This is separate from omitting move_selector inside an explicit local-search phase. VND is never prepended implicitly.

Construction Heuristic

Builds an initial solution by completing required planning work and deciding optional assignments. Optional variables may legally remain unassigned. Runs before any eligible or explicitly configured local search.

Construction Heuristic Types

Type Description
first_fit Default generic first-fit construction. Scalar-only targets use the compiled descriptor-placement schedule; mixed/list-bearing targets use the compiled global scan.
first_fit_decreasing Specialized scalar-only 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 Generic best-score construction. Scalar-only targets use the compiled descriptor-placement schedule; mixed/list-bearing targets use the compiled global scan.
allocate_entity_from_queue Queue-driven entity allocation.
allocate_to_value_from_queue Queue-driven value allocation.
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"

The stock runtime resolves one RuntimeModel per planning model and compiles the full phase sequence before execution. Construction stages, local-search selector trees, providers, defaults, and stable source bindings are immutable for that solve. Scalar-only, list-only, mixed, native, and dynamic models do not split into parallel phase-builder lifecycles.

Reached construction nodes prepare their current solve-owned sources lazily; already-terminated or unreached nodes do not bind unused sources. Phase-local termination overlays begin and end at their top-level phase boundary. Mandatory omitted construction does not install a phase-local overlay; it remains governed by the retained job’s top-level limits. If one of those limits fires before structural completion, the job fails without publishing a partial best solution or snapshot.

Scalar-only construction heuristics validate their model-owned hooks before phase build. first_fit_decreasing and allocate_entity_from_queue require construction_entity_order_key; weakest_fit, strongest_fit, and allocate_to_value_from_queue require construction_value_order_key; decreasing weakest/strongest fit require both. Those keys are evaluated against the live working solution at each construction step.

Nullable scalar construction defaults to construction_obligation = "preserve_unassigned", so an optional variable may keep None when that is the best legal baseline. Use assign_when_candidate_exists only when construction must assign any doable candidate instead.

When group_name selects an assignment-backed ScalarGroup, grouped scalar construction also uses construction_obligation: with assign_when_candidate_exists, every required nullable scalar slot that has a doable candidate is treated as a construction obligation.

Iteratively improves the solution by applying moves and accepting improvements (and sometimes worse moves to escape local optima).

Current local-search telemetry emits phase_start after the starting score is known. With console output enabled, the phase-start line includes that score, making it clear what construction handed to local search before the first move is evaluated.

SolverTelemetry::phases retains every started phase in execution order, with phase_index and phase_type. Each PhaseTelemetry carries active elapsed time, steps, generated, evaluated, speculative, accepted, applied, improving, score-calculation, generation-time, and evaluation-time counters. A progress snapshot’s last entry is the running phase; terminal telemetry keeps completed entries. Run totals also include work outside phases, such as initial scoring. Long phases publish metadata-only pulses. Pause and cancel settle both inside large candidate work and around phase/terminal hooks; paused time is excluded from active phase elapsed time.

Compiled acceptor-forager local search supports ordered parallel evaluation through top-level move_thread_count; VND and other phase families remain sequential. See Local Search for evaluator ceilings, speculative work, and reproducibility boundaries.

Acceptors

Local search uses an acceptor to decide whether to keep a move. In the stock config surface, 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

Move Selector and Forager

Local search phases can also specify a selector and forager:

[[phases]]
type = "local_search"

[phases.acceptor]
type = "late_acceptance"
late_acceptance_size = 400

[phases.move_selector]
type = "change_move_selector"
variable_name = "employee_idx"

[phases.forager]
type = "accepted_count"
limit = 32

The accepted-count forager stops the current selector step after collecting limit accepted candidates, then picks the best candidate inside that step horizon. Use best_score when you intentionally want a full-neighborhood scan.

Assignment-backed scalar repair is configured through the grouped scalar move selector:

[phases.move_selector]
type = "grouped_scalar_move_selector"
group_name = "required_shift_assignment"
max_moves_per_step = 64
require_hard_improvement = true

The selector consumes the same named assignment-backed ScalarGroup used by construction, then emits compound scalar repair moves for uncovered required slots, capacity conflicts, bounded reassignments, and sequence/position rematches.

Acceptor-Specific Configuration

Simulated Annealing:

[phases.acceptor]
type = "simulated_annealing"
level_temperatures = [5.0, 500.0]
hard_regression_policy = "temperature_controlled"

[phases.acceptor.calibration]
sample_size = 64
target_acceptance_probability = 0.75
fallback_temperature = 2.0

Simulated annealing is score-level aware. level_temperatures are ordered from highest-priority score level to lowest; if they are omitted, calibration can sample candidate deltas and derive temperatures per level. Once the configured temperature cools to the hill-climbing threshold, worsening moves are rejected deterministically.

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

Variable Neighborhood Descent

Variable Neighborhood Descent is an ordered-neighborhood local-search type. It runs several neighborhoods in sequence and restarts from the first neighborhood whenever an improvement is found.

[[phases]]
type = "local_search"
local_search_type = "variable_neighborhood_descent"

[[phases.neighborhoods]]
type = "change_move_selector"
variable_name = "employee_idx"

[[phases.neighborhoods]]
type = "swap_move_selector"
variable_name = "employee_idx"

When one neighborhood needs a hard move cap, wrap that neighborhood itself with limited_neighborhood rather than decorating a broader selector tree:

[[phases.neighborhoods]]
type = "limited_neighborhood"
selected_count_limit = 48

[phases.neighborhoods.selector]
type = "change_move_selector"
variable_name = "employee_idx"

Exact tree search exists as a typed extension point for small finite spaces. It is not a stock solver.toml phase that can be built from generated scalar slots alone. Register exact search as a named custom phase through #[planning_solution(search = "...")], then order that compiled-in phase from config.

The lower-level API exposes ExhaustiveSearchPhase, ExhaustiveSearchConfig, ExplorationType, and SimpleDecider for applications that own the concrete decider. Use this path only when the domain can provide a real exact-search state expansion and pruning strategy.

[[phases]]
type = "custom"
name = "small_exact_search"

Partitioned search is available when the application provides a typed SolutionPartitioner that splits and merges independent subproblems. SolverForge does not infer safe partitions from a count.

[[phases]]
type = "partitioned_search"
partitioner = "by_vehicle"
thread_count = "auto"
log_progress = true

Partition children inherit cancellation, remaining time, environment mode, in-phase limits, and deterministic child seeds. Retained-job publication stays on the parent full-solution scope; pause checkpoints are emitted only by the parent.

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