Reference
Engineer-facing reference for tuning SolverForge behavior without losing the stock runtime path.
Extend the Solver
Start with the stock runtime and make it earn the next abstraction. Most SolverForge apps need better domain modeling and better constraints before they need custom solver machinery.
The default rule
Change the solver in this order:
- fix the domain model
- fix the constraints and score weights
- tune
solver.toml - add custom runtime pieces only when configuration stops being enough
That order keeps the application understandable and lets you keep using the well-tested retained runtime path.
What belongs where
| Concern | Best home |
|---|---|
| business rules and penalties | constraint code |
| search strategy and runtime limits | solver.toml |
| per-job adjustments | a #[planning_solution(config = "...")] callback layering on top of the loaded config |
| UI-specific progress display | the edge layer, not the runtime |
| experimental custom phases or selectors | app-side code using the lower-level crates |
Canonical selector defaults
If move_selector is omitted, the stock runtime stays intentionally narrow:
- scalar-only models default to
ChangeMoveSelectorplusSwapMoveSelector - list-only models default to
NearbyListChangeMoveSelector(20),NearbyListSwapMoveSelector(20),SublistChangeMoveSelector,SublistSwapMoveSelector, andListReverseMoveSelector, with k-opt and list ruin enabled only when their hooks exist - mixed models use the list defaults first, then scalar defaults
Assignment-owned scalar variables stay on their grouped scalar selector path.
Plain scalar defaults and conflict-repair defaults exclude slots owned by an
assignment-backed ScalarGroup.
limited_neighborhood is the tool for putting a hard cap on one neighborhood
that is otherwise too broad. It is not a substitute for understanding the
search policy you are expressing.
Tune in this order
| Tuning step | Use it for |
|---|---|
| construction phase choice | initial feasibility and seed quality |
| local search acceptor | exploration vs greediness |
| move selector choice | neighborhood breadth and cost |
accepted-count limit |
finite accepted-candidate horizon for one selector step |
value_candidate_limit |
bounded scalar value generation for selectors that support it |
| termination limits | wall time, unimproved steps, or best-score goals |
| VND / typed exact / partitioned search | explicit advanced search strategies, not a default reflex |
Nearby scalar selectors require model-declared candidate hooks. Use
nearby_value_candidates for nearby change, nearby_entity_candidates for
nearby swap, and distance meters only to rank or filter those bounded
candidates.
When custom code is justified
Write custom solver code when one of these is true:
- the stock phases cannot express the search policy you need
- the neighborhood generator must encode domain-specific structure that config cannot capture
- you need app-specific orchestration around retained jobs and snapshots
- a lower-level crate gives you leverage that the facade intentionally hides
If you go there, keep the blast radius small. Prefer one app-side extension over forking the scaffold or bypassing the retained runtime wholesale.
Custom search is compiled into the solution with
#[planning_solution(search = "...")]; solver.toml names registered phases
instead of loading arbitrary runtime classes. Partitioned search similarly
requires a typed SolutionPartitioner, not a partition count guessed from the
outside.
Telemetry and lifecycle expectations
Retained jobs now expose exact counts and durations through structured events. That means:
- generated, evaluated, and accepted move counts belong to runtime telemetry
- not-doable, acceptor-rejected, forager-ignored, hard-delta, conflict-repair, and construction-slot counters belong there too
- selector telemetry carries stable selector indexes and labels for local-search and VND diagnosis
- generation and evaluation durations stay exact in the event stream
- move-label telemetry and the bounded applied-move trace belong to runtime diagnostics, not benchmark-only instrumentation
moves/sis a display-only derived metric at the UI edge- pause, resume, snapshot fetch, and analysis should use the retained
SolverManagercontract rather than ad-hoc channels
Practical checklist
- keep the domain model and config separate
- only use a config callback to decorate loaded config, not replace it blindly
- tune constraints before tuning search
- benchmark any custom neighborhood work before adopting it permanently
- preserve structured events so
solverforge-uiand service layers stay honest