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@=0.27.0

These pages track solverforge 0.27.0, and every Rust example on them compiles against that release. The workspace declares Rust 1.95.

For end-to-end app scaffolding, prefer the standalone solverforge-cli workflow. solverforge-cli 4.0.4 scaffolds solverforge 0.26.1; an app scaffolded by it or by an earlier CLI pins an older core, so read the solverforge version in its Cargo.toml and see the 0.27.x upgrade checklist before moving it to 0.27.0.

cargo install solverforge-cli
solverforge new my-scheduler
cd my-scheduler
solverforge server

How It Fits Together

A model is a set of Rust structs marked with derive macros and registered by a planning_model! manifest; see Domain Modeling. Constraints are typed streams returned as a constraint set; see Constraints. At solve time the generated runtime resolves one RuntimeModel for the planning model and compiles construction, selectors, providers, and defaults into one immutable search graph before the first phase runs, so an invalid declaration fails before any candidate work. SolverManager runs solves as retained jobs and streams their events.

Solver configuration comes from #[planning_solution(solver_toml = "...")], which embeds the file at compile time, or from a SolverConfig passed at solve time. A model with neither uses the defaults: construction runs, and a local-search phase is added only when termination is configured. See Solver Configuration.

Minimal Example

A binary crate with cargo add solverforge@=0.27.0 and three files. src/domain/mod.rs registers the model:

solverforge::planning_model! {
    root = "src/domain";

    mod plan;

    pub use plan::{Plan, Task, Worker};
}

src/domain/plan.rs holds the model and its constraints:

use solverforge::prelude::*;

#[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"),
    )
}

src/main.rs solves a plan with two workers and three tasks:

mod domain;

use domain::{Plan, Task, Worker};
use solverforge::{SolverEvent, SolverManager};

static MANAGER: SolverManager<Plan> = SolverManager::new();

fn main() {
    let problem = Plan {
        workers: vec![
            Worker { id: 0, name: "Amina".into() },
            Worker { id: 1, name: "Bruno".into() },
        ],
        tasks: (0..3).map(|id| Task { id, worker: None }).collect(),
        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 { .. } | SolverEvent::BestSolution { .. } => {}
            SolverEvent::Completed { metadata, solution } => {
                println!("finished with reason {:?}", metadata.terminal_reason);
                println!("score {:?}", solution.score);
                let assigned = solution.tasks.iter().filter(|task| task.worker.is_some()).count();
                println!("{assigned} of {} tasks assigned", solution.tasks.len());
                break;
            }
            SolverEvent::Failed { error, .. } => panic!("solve failed: {error}"),
            SolverEvent::Cancelled { .. } => break,
            SolverEvent::PauseRequested { .. } | SolverEvent::Paused { .. } | SolverEvent::Resumed { .. } => {}
        }
    }

    MANAGER.delete(job_id).expect("delete retained job");
}

cargo run prints:

finished with reason Some(Completed)
score Some(HardSoftScore(0, 0))
3 of 3 tasks assigned

API Reference

Consult the v0.27.0 implementation and the repository crates/*/WIREFRAME.md API maps for this source line. docs.rs/solverforge serves published crate APIs; select the version matching your dependency.

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