Documentation
SolverForge
Native Rust constraint solver.
SolverForge is a native Rust constraint solver for planning and scheduling problems. It uses derive macros for domain modeling, constraint streams for declarative rule definition, and metaheuristic algorithms for optimization.
Installation
cargo add solverforge
These pages track the solverforge 0.17.2 crate and current release
workspace. Generated CLI projects can intentionally target an older scaffold
runtime; the published solverforge-cli 2.2.2 package scaffolds
solverforge 0.15.2, so check solverforge --version when starting from a
scaffold.
For end-to-end app scaffolding, prefer the standalone
solverforge-cli workflow:
cargo install solverforge-cli
solverforge new my-scheduler
cd my-scheduler
solverforge server
The 0.17.2 crate declares Rust 1.95.
The generated runtime now builds one RuntimeModel for each planning model.
Scalar metadata is resolved by descriptor index and variable name, not by Rust
module declaration order. Generic FirstFit and CheapestInsertion use the
canonical construction engine whenever matching list work is present, while pure
scalar matches reuse the descriptor boundary. Assignment-backed grouped scalar
construction can cover required nullable scalar slots through
ScalarGroup::assignment(...), and optional scalar variables keep None when
it is the best legal baseline unless configuration asks construction to assign
whenever a candidate exists.
Startup telemetry is shape-aware in the current release: scalar solves report
average candidates, list solves report element counts, and console output
labels those solve shapes as candidates or elements.
The current release tightens several public contracts:
#[solverforge_constraints]is the canonical constraint-function attribute when a function reuses grouped streams. Reused same-binding grouped streams and syntax-proved identical grouped chains share one retained node while each.named(...)terminal keeps its own identity, ordering, metadata, and score explanation- generated collection sources are solution-associated methods such as
Schedule::shifts(), and stream roots useConstraintFactory::for_each(Schedule::shifts()) - assignment-backed grouped scalar construction and repair are public runtime
policy through
ScalarGroup::assignment(...), grouped constructiongroup_name, andgrouped_scalar_move_selector - solver construction internals that advanced integrations use directly now
expose
GroupedScalarCursor,GroupedScalarSelector,ScalarAssignmentMoveCursor,ScalarAssignmentMoveOptions, andScalarAssignmentRequiredStreamingCursorfrom the public solver surface collect_vec(...),consecutive_runs(...),indexed_presence(...),CollectedVec,IndexedPresence,Run, andRunsare available from the prelude for grouped collection, streak, and ordinal-presence rules; their sharedCollector<Input>contract covers unary rows, projected rows, and joined cross-join pairs- scoring terminals use
penalize(score),reward(score), typed dynamic closures,fixed_weight(...), andhard_weight(...); the formerpenalize_hard,penalize_with, andreward_softhelper family is no longer part of the current stream API - solver configuration controls such as
SolverConfig,PhaseConfig,MoveSelectorConfig,AcceptorConfig,ForagerConfig,SolverConfigOverride, and related enums are available directly fromsolverforge solverforge::bridgere-exports host-language binding contracts for logical entity/fact/variable IDs, dynamic score families, and descriptor-resolved dynamic scalar/list slots- projected scoring rows use
Projection/ProjectionSinkfor bounded single-source rows, and cross joins can either group joined pairs directly with.group_by(|left, right| key, collector)or retain one scoring row per joined pair with.project(|left, right| row) - projected streams support symmetric self-joins with
equal(|row| key)and directed same-output joins withequal_bi(left_key, right_key)when pair orientation is part of the rule - direct cross-join grouped streams can call
complement(...)against a generated fact or entity source, so missing target keys produce explicit default rows without a projected-row detour - filtered keyed joins preserve the filter contract on both joined sources, flattened keyed targets, projected joined rows, and complement sources
- projected outputs, projected self-join keys, and grouped collector values no
longer require
Clone - projected self-join ordering is coordinate-stable by source ownership and emission index, with low-level joined filters receiving primary owner entity indexes rather than retained storage row IDs
- scalar construction order is model-owned through
construction_entity_order_keyandconstruction_value_order_key; those hooks are evaluated against the live working solution at each construction step and do not reorder local-search candidates - nearby scalar neighborhoods are bounded model capabilities through
nearby_value_candidatesandnearby_entity_candidates; distance meters rank or filter those candidates, but do not discover them - default local-search neighborhoods are explicit streaming defaults: nearby scalar selectors when hooks exist, scalar change/swap fallbacks for non-assignment-owned slots, list nearby-change, nearby-swap, sublist, reverse, optional k-opt/list-ruin when hooks exist, and grouped-scalar or conflict-repair selectors only when the model declares them
- assignment-backed grouped scalar search includes bounded value-window swaps, longer window swaps, same-sequence run-gap swaps, block reassignments, optional run releases, and value rotations without weakening the assignment hard constraints
- stock CVRP route lists can now declare
#[planning_list_variable(element_collection = "...", domain = "cvrp")]. The profile expands tosolverforge::cvrp::VrpSolution, the stock CVRP distance meters, route hooks, savings hooks, and savings metric class. Route-local phases such as k-opt use strict stock CVRP feasibility, while Clarke-Wright construction uses relaxed savings admissibility so capacity and time-window violations can still be compared by the score model - stock CVRP distance hooks reject unreachable travel-time legs in strict route feasibility and convert unreachable or malformed distance entries into a large finite construction/search cost instead of panicking or overflowing
- route-distance arithmetic used by Clarke-Wright and k-opt is clamped, so unreachable or extreme matrix values stay in the solver’s scoring domain
- custom routing domains can still omit
domain = "cvrp"and wireroute_hooks,savings_hooks, and optionalsavings_metric_class_fnexplicitly when they need non-CVRP semantics or different construction pruning policies ListClarkeWrightconstruction completes unmatched route elements instead of dropping them when no saving merge can place them- list variables can declare fixed element ownership, construction element
ordering, and fixed precedence hooks; the stock runtime exposes
ListPrecedenceMakespanConstraint,list_permute_move_selector, andlist_precedence_move_selectorfor generic precedence-list models. Cheapest-insertion list construction uses precedence duration/successor hooks to dispatch downstream-critical unassigned elements earlier when the hooks are present - typed custom search is compiled into the solution with
#[planning_solution(search = "...")]; config names registered phases instead of loading arbitrary runtime classes - retained telemetry preserves exact generated, evaluated, accepted,
not-doable, acceptor-rejected, forager-ignored, hard-delta, conflict-repair,
construction-slot, move-label, and bounded applied-move trace counters plus
generation and evaluation durations;
moves/sis only a display metric
Minimal Example
use solverforge::prelude::*;
use solverforge::{SolverEvent, SolverManager};
use solverforge::stream::ConstraintFactory;
#[problem_fact]
pub struct Worker {
#[planning_id]
pub id: usize,
pub name: String,
}
#[planning_entity]
pub struct Task {
#[planning_id]
pub id: usize,
#[planning_variable(value_range_provider = "workers", allows_unassigned = true)]
pub worker: Option<usize>,
}
#[planning_solution(constraints = "define_constraints")]
pub struct Plan {
#[problem_fact_collection]
pub workers: Vec<Worker>,
#[planning_entity_collection]
pub tasks: Vec<Task>,
#[planning_score]
pub score: Option<HardSoftScore>,
}
#[solverforge_constraints]
fn define_constraints() -> impl ConstraintSet<Plan, HardSoftScore> {
(
ConstraintFactory::<Plan, HardSoftScore>::new()
.for_each(Plan::tasks())
.unassigned()
.penalize(HardSoftScore::ONE_HARD)
.named("Unassigned task"),
)
}
static MANAGER: SolverManager<Plan> = SolverManager::new();
fn main() {
let problem = Plan {
workers: vec![],
tasks: vec![],
score: None,
};
let (job_id, mut rx) = MANAGER.solve(problem).expect("solver job should start");
while let Some(event) = rx.blocking_recv() {
match event {
SolverEvent::Progress { metadata } => {
println!("best so far: {:?}", metadata.best_score);
}
SolverEvent::BestSolution { metadata, .. } => {
println!("new best at snapshot {:?}", metadata.snapshot_revision);
}
SolverEvent::Completed { metadata, .. } => {
println!("finished with reason {:?}", metadata.terminal_reason);
break;
}
SolverEvent::Cancelled { .. } | SolverEvent::Failed { .. } => break,
SolverEvent::PauseRequested { .. } | SolverEvent::Paused { .. } | SolverEvent::Resumed { .. } => {}
}
}
let snapshot = MANAGER
.get_snapshot(job_id, None)
.expect("latest snapshot should exist");
println!("latest snapshot revision {}", snapshot.snapshot_revision);
MANAGER.delete(job_id).expect("delete retained job");
}
API Reference
Full published API documentation is available on
docs.rs/solverforge. docs.rs can briefly lag the
crate registry after a release; the 0.17.2 crate is the source of truth once
crates.io has accepted the package. Source-line API maps for the local
workspace live in the repository crates/*/WIREFRAME.md files.
Sections
- Domain Modeling — Derive macros for solutions, entities, and problem facts
- Constraints — Constraint streams, projected scoring rows, existence, joiners, collectors, and score types
- Solver — Configuration, construction, local search, moves, termination, and SolverManager