Documentation
Configuration
Runtime configuration with SolverConfig, solver.toml, and parsing helpers.
The stock generated runtime loads solver.toml automatically when you call
SolverManager::solve(...). In the current release, the solverforge facade
also exports the normal configuration API directly, so app code can stay on one
public dependency when it needs to inspect or build configs directly.
use solverforge::{
AcceptorConfig, CandidateTraceConfig, ForagerConfig, MoveSelectorConfig, PhaseConfig,
SolverConfig, SolverConfigOverride,
};
Configuration has three levels:
| Level | Scope | Typical owner |
|---|---|---|
| Global config | environment mode, random seed, thread count, top-level termination, candidate tracing | app/operator |
| Phase config | construction, local search, score ties, VND via local_search_type, partitioned search, custom phase names, phase-specific termination |
app/operator |
| Model capabilities | candidate providers and metrics, nearby hooks, list operations/metadata, construction order keys, scalar groups | Rust model or binding |
The important rule is that config selects declared capabilities. It does not invent model hooks. If a selector asks for nearby scalar candidates, grouped scalar candidates, assignment-backed scalar groups, or conflict repair providers, the model must expose those capabilities through the generated model support layer.
SolverForge 0.19.1 compiles the complete resolved policy before solving. A bad target, missing operation bundle, unknown provider or metric, invalid selector composition, or inconsistent construction source is a build error at its config path; the runtime does not defer schema discovery until a hot candidate loop.
Loading Configuration
From a TOML file
use solverforge::SolverConfig;
let config = SolverConfig::load("solver.toml").unwrap();
From a TOML string
use solverforge::SolverConfig;
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();
From YAML
use solverforge::SolverConfig;
let config = SolverConfig::from_yaml_str(r#"
environment_mode: reproducible
termination:
seconds_spent_limit: 120
phases:
- type: construction_heuristic
construction_heuristic_type: first_fit
"#).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"
construction_obligation = "preserve_unassigned"
value_candidate_limit = 32
[[phases]]
type = "local_search"
[phases.acceptor]
type = "simulated_annealing"
level_temperatures = [5.0, 500.0]
hard_regression_policy = "never_accept_hard_regression"
[phases.move_selector]
type = "change_move_selector"
variable_name = "employee_id"
value_candidate_limit = 32
[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 (default)
move_thread_count = "none" # Single-threaded
Random Seed
Set a fixed seed when you want reproducible runs:
random_seed = 42
Candidate Trace
Candidate tracing is an opt-in diagnostic surface. It records a bounded ordered prefix at the engine candidate-pull boundary:
[candidate_trace]
max_entries = 4096
Each retained pull can carry its phase and step, selector/source index, logical
operation identity, construction target, and ordered disposition transitions
such as evaluated, rejected, selected, and applied. The header owns canonical
config, execution policy, resolved phase plan, and optional externally attested
input/qualification provenance. max_entries must be nonzero; truncation is
reported explicitly.
Ordinary events, status, and snapshots stay compact. Read the trace together
with the exact aggregate publication using
SolverManager::get_telemetry_detail(job_id).
Phases
Phases run in sequence. A typical configuration uses a construction heuristic followed by local search:
[[phases]]
type = "construction_heuristic"
construction_heuristic_type = "first_fit"
[[phases]]
type = "local_search"
score_tie_break = "random"
[phases.acceptor]
type = "tabu_search"
entity_tabu_size = 7
See Phases for all phase types and their options.
Phase Anatomy
Most production configs have this shape:
[[phases]]
type = "construction_heuristic"
construction_heuristic_type = "first_fit"
construction_obligation = "preserve_unassigned"
value_candidate_limit = 32
[[phases]]
type = "local_search"
[phases.acceptor]
type = "late_acceptance"
late_acceptance_size = 400
[phases.forager]
type = "accepted_count"
limit = 4
[phases.move_selector]
type = "union_move_selector"
selection_order = "stratified_random"
weighting = "equal"
[[phases.move_selector.selectors]]
type = "change_move_selector"
value_candidate_limit = 32
selection_order = "random"
[[phases.move_selector.selectors]]
type = "swap_move_selector"
[phases.termination]
step_count_limit = 100000
Construction creates the first workable solution. Local search improves it.
The acceptor decides whether a scored candidate can be accepted. The forager
decides which accepted candidate is committed. The move selector decides which
candidate moves are pulled. score_tie_break = "random" is the seeded default
for equally scored best candidates; use first to retain the first equal best
candidate instead.
Move Selectors
For config-driven local search, move selection lives under
[phases.move_selector].
[phases.move_selector]
type = "nearby_list_change_move_selector"
max_nearby = 12
variable_name = "visits"
Nearby selection is configured by choosing a nearby selector variant, not by
top-level nearby_selection = true flags.
Every non-composite leaf selector accepts an optional selection_order:
| Value | Behavior |
|---|---|
original |
preserve the canonical cursor order |
random |
seeded random pull order; used by omitted local-search defaults |
shuffled |
seeded full shuffle |
sorted |
sort a complete stream by a registered selection_metric |
probabilistic |
sample a complete stream by non-negative weights from a registered selection_metric |
[phases.move_selector]
type = "change_move_selector"
variable_name = "employee_id"
selection_order = "sorted"
selection_metric = "assignment_priority"
sorted and probabilistic require a named runtime candidate metric;
selection_metric is rejected for the other orders. Advanced native or binding
runtimes register immutable metrics on
RuntimeModel::with_candidate_metrics(...). Unknown names, non-finite results,
and negative probabilistic weights fail explicitly.
Union selectors have their own scheduling policy. selection_order can be
sequential, round_robin, rotating_round_robin, random, or the default
stratified_random. weighting is equal by default, fixed with a weights
vector, or candidate_count to derive child weights from declared candidate
counts:
[phases.move_selector]
type = "union_move_selector"
selection_order = "stratified_random"
weighting = "fixed"
weights = [3, 1]
[[phases.move_selector.selectors]]
type = "change_move_selector"
[[phases.move_selector.selectors]]
type = "swap_move_selector"
Nearby scalar selectors require model-declared candidate hooks on the matching
#[planning_variable]: nearby_value_candidates for
nearby_change_move_selector and nearby_entity_candidates for
nearby_swap_move_selector. Distance meters only rank or filter those bounded
candidates.
List selectors likewise select declared list capabilities. list_permute_move_selector
permutes a bounded window in one list. list_precedence_move_selector targets
list variables that expose precedence_duration_fn and
precedence_successors_fn hooks through #[planning_list_variable]; it streams
critical-path support moves for generic precedence makespan models.
[phases.move_selector]
type = "list_permute_move_selector"
variable_name = "operations"
min_window_size = 2
max_window_size = 5
[phases.move_selector]
type = "list_precedence_move_selector"
entity_class = "Route"
variable_name = "operations"
When you need to cap one neighborhood deliberately, use the
limited_neighborhood selector variant and wrap the concrete selector inside
it:
[phases.move_selector]
type = "limited_neighborhood"
selected_count_limit = 24
[phases.move_selector.selector]
type = "change_move_selector"
variable_name = "employee_id"
That cap applies to the wrapped neighborhood itself. It is separate from the
accepted-count forager limit, which stops a selector step after that many
accepted candidates and then picks the best candidate inside that step horizon.
Scalar change_move_selector, nearby_change_move_selector,
pillar_change_move_selector, and ruin_recreate_move_selector accept
value_candidate_limit. Scalar cheapest_insertion requires a bounded
candidate source: either candidate_values on the model or this config limit.
Grouped scalar construction and grouped scalar local search use group_name
when the model provides a scalar group through the generated model support
surface. Grouped local search also supports require_hard_improvement when a
compound candidate must improve the hard score before it can be accepted.
Assignment-backed scalar construction and repair use the same group_name
surface. The model declares ScalarGroup::assignment(...); construction
generates stock nullable scalar assignment candidates and local search repairs
uncovered required entities, capacity conflicts, bounded reassignments, and
sequence/position rematches:
[[phases]]
type = "construction_heuristic"
construction_heuristic_type = "first_fit"
construction_obligation = "assign_when_candidate_exists"
group_name = "required_shift_assignment"
value_candidate_limit = 8
group_candidate_limit = 64
[[phases]]
type = "local_search"
[phases.move_selector]
type = "grouped_scalar_move_selector"
group_name = "required_shift_assignment"
max_moves_per_step = 64
require_hard_improvement = true
Grouped construction example:
[[phases]]
type = "construction_heuristic"
construction_heuristic_type = "first_fit"
group_name = "task_operator_assignment"
value_candidate_limit = 32
group_candidate_limit = 128
Grouped local-search example:
[phases.move_selector]
type = "grouped_scalar_move_selector"
group_name = "task_operator_assignment"
value_candidate_limit = 32
max_moves_per_step = 256
require_hard_improvement = true
Conflict-repair selectors configure constraints by exact scoring metadata
identity. Use ConstraintRef::full_name() for package-qualified constraints and
the short name for package-less constraints. With include_soft_matches = false,
soft constraints are rejected before providers run; setting it to true
explicitly allows soft repair providers. Conflict repair operates on
non-assignment-owned scalar variables; assignment-backed scalar slots stay on
their owning grouped scalar selector path.
Compound repair example:
[phases.move_selector]
type = "compound_conflict_repair_move_selector"
constraints = ["schedule/no_overlapping_operator_assignment"]
max_matches_per_step = 16
max_repairs_per_match = 32
max_moves_per_step = 256
require_hard_improvement = true
Variable Neighborhood Descent
VND is a local-search type, not a standalone type = "vnd" phase:
[[phases]]
type = "local_search"
local_search_type = "variable_neighborhood_descent"
[[phases.neighborhoods]]
type = "change_move_selector"
variable_name = "employee_id"
[[phases.neighborhoods]]
type = "swap_move_selector"
variable_name = "employee_id"
The variable_neighborhood_descent mode uses ordered neighborhoods and rejects
acceptor, forager, and move_selector. The default acceptor_forager
local-search mode uses acceptor, forager, and move_selector.
Partitioned And Custom Search
partitioned_search requires a named partitioner compiled into the solution’s
typed search surface. SolverForge does not infer partitions from a count:
[[phases]]
type = "partitioned_search"
partitioner = "by_vehicle"
thread_count = "auto"
log_progress = true
Custom phases are also compiled into the solution with
#[planning_solution(search = "...")] and selected by name:
[[phases]]
type = "custom"
name = "weekend_repair"
There is no arbitrary custom_phase_class loader or erased plugin registry.
Construction Obligation
Nullable scalar variables default to preserve_unassigned: construction may
leave None in place when that is legal and scores best. Use
assign_when_candidate_exists when construction should assign a doable value
whenever one exists:
[[phases]]
type = "construction_heuristic"
construction_heuristic_type = "first_fit"
construction_obligation = "assign_when_candidate_exists"
Termination
Controls when the solver stops. See Termination for all options.
[termination]
seconds_spent_limit = 300
Configured solver and phase limits remain binding during required scalar and
list construction. SolverForge publishes a best solution only after every
mandatory list element, required assignment row, and non-optional scalar slot
is assigned. If a limit fires first, the retained job ends as Failed without
publishing an incomplete solution or snapshot.
Programmatic Builders
SolverConfig also exposes simple builder helpers:
let config = SolverConfig::new()
.with_random_seed(42)
.with_termination_seconds(30);
Per-Solution Overlays
Macro-generated retained solves can layer runtime policy on top of the loaded
solver.toml by using config = "..." on #[planning_solution]:
#[planning_solution(
constraints = "define_constraints",
config = "solver_config_for_solution"
)]
pub struct Schedule {
#[planning_score]
pub score: Option<HardSoftScore>,
pub time_limit_secs: u64,
}
fn solver_config_for_solution(solution: &Schedule, config: SolverConfig) -> SolverConfig {
config.with_termination_seconds(solution.time_limit_secs)
}
The callback receives the already loaded solver.toml config, so it should
decorate that base config rather than replace it from scratch.
See Also
- Construction — Construction policy and grouped scalar construction
- Local Search — Acceptors, foragers, and move selectors
- Moves — Selector families and move behavior
- Termination — Termination conditions
- SolverManager — Running the solver