Solver
Configure and run the constraint solver
This section covers solver configuration and execution.
In This Section
- Configuration - Configure solver behavior with
SolverConfig and TerminationConfig - Factory - Create and run solvers with
SolverFactory and Solver - Scores - Understand
SimpleScore, HardSoftScore, HardMediumSoftScore, and BendableScore
Overview
Running a solver involves:
- Configure - Set termination conditions, environment mode, and threading
- Build Request - Combine domain, constraints, WASM, and problem data
- Execute - Send to solver service and receive solution
- Interpret - Parse score and solution
use solverforge_core::{SolveRequest, SolveResponse, TerminationConfig};
// Build request with termination
let request = SolveRequest::new(
domain, constraints, wasm_base64,
"alloc", "dealloc", list_accessor, problem_json
)
.with_termination(TerminationConfig::new()
.with_spent_limit("PT5M")
.with_best_score_feasible(true)
);
// Send to solver
let response: SolveResponse = client
.post(&format!("{}/solve", service.url()))
.json(&request)
.send()?
.json()?;
// Check result
println!("Score: {}", response.score);
if response.score.starts_with("0hard") {
println!("Solution is feasible!");
}
Termination Conditions
The solver continues until a termination condition is met:
| Condition | Description |
|---|
spent_limit | Maximum solving time (e.g., "PT5M") |
best_score_feasible | Stop when feasible |
best_score_limit | Stop at target score |
move_count_limit | Maximum moves |
step_count_limit | Maximum steps |
Multiple conditions can be combined - the solver stops when any is satisfied.
1 - Solver Configuration
Configure solver behavior with SolverConfig and TerminationConfig
Configure how the solver runs with SolverConfig and TerminationConfig.
SolverConfig
Configure overall solver behavior:
use solverforge_core::{SolverConfig, TerminationConfig, EnvironmentMode, MoveThreadCount};
let config = SolverConfig::new()
.with_solution_class("Schedule")
.with_entity_class("Shift")
.with_environment_mode(EnvironmentMode::Reproducible)
.with_random_seed(42)
.with_move_thread_count(MoveThreadCount::Auto)
.with_termination(TerminationConfig::new()
.with_spent_limit("PT5M")
);
SolverConfig Methods
| Method | Description |
|---|
with_solution_class(class) | Set the solution class name |
with_entity_class(class) | Add an entity class |
with_entity_classes(classes) | Set all entity classes |
with_environment_mode(mode) | Set environment mode |
with_random_seed(seed) | Set random seed for reproducibility |
with_move_thread_count(count) | Set parallel thread count |
with_termination(config) | Set termination configuration |
Environment Modes
| Mode | Description |
|---|
Reproducible | Same seed = same solution (default) |
NonReproducible | Random behavior each run |
NoAssert | Minimal validation (fastest) |
PhaseAssert | Validate after each phase |
StepAssert | Validate after each step |
FullAssert | Maximum validation (slowest) |
TrackedFullAssert | Full validation with tracking |
Move Thread Count
| Option | Description |
|---|
MoveThreadCount::Auto | Use available CPUs |
MoveThreadCount::None | Single-threaded |
MoveThreadCount::Count(n) | Specific thread count |
TerminationConfig
Define when the solver should stop:
let termination = TerminationConfig::new()
.with_spent_limit("PT5M") // Max 5 minutes
.with_unimproved_spent_limit("PT30S") // Stop if no improvement for 30s
.with_best_score_feasible(true) // Stop when feasible
.with_move_count_limit(10000); // Max 10,000 moves
Time-Based Termination
Use ISO-8601 duration format:
// Duration format: PT{hours}H{minutes}M{seconds}S
.with_spent_limit("PT5M") // 5 minutes
.with_spent_limit("PT1H30M") // 1 hour 30 minutes
.with_spent_limit("PT10S") // 10 seconds
.with_unimproved_spent_limit("PT30S") // No improvement timeout
Score-Based Termination
// Stop when feasible (hard score >= 0)
.with_best_score_feasible(true)
// Stop at specific score
.with_best_score_limit("0hard/-100soft")
Count-Based Termination
.with_step_count_limit(1000) // Max solver steps
.with_move_count_limit(10000) // Max moves
.with_unimproved_step_count(100) // Steps without improvement
.with_score_calculation_count_limit(1000000) // Score calculations
Diminished Returns
Stop when improvements become too small:
use solverforge_core::DiminishedReturnsConfig;
let dr = DiminishedReturnsConfig::new()
.with_minimum_improvement_ratio("0.001")
.with_slow_improvement_limit("PT30S");
let termination = TerminationConfig::new()
.with_diminished_returns(dr);
TerminationConfig Methods
| Method | Description |
|---|
with_spent_limit(duration) | Maximum solving time |
with_unimproved_spent_limit(duration) | Timeout without improvement |
with_unimproved_step_count(count) | Steps without improvement |
with_best_score_limit(score) | Target score to reach |
with_best_score_feasible(bool) | Stop when hard >= 0 |
with_step_count_limit(count) | Maximum steps |
with_move_count_limit(count) | Maximum moves |
with_score_calculation_count_limit(count) | Max score calculations |
with_diminished_returns(config) | Diminishing returns config |
Complete Example
let config = SolverConfig::new()
.with_solution_class("Schedule")
.with_entity_class("Shift")
.with_environment_mode(EnvironmentMode::Reproducible)
.with_random_seed(42)
.with_termination(
TerminationConfig::new()
.with_spent_limit("PT10M") // Max 10 minutes
.with_unimproved_spent_limit("PT1M") // No improvement for 1 min
.with_best_score_feasible(true) // Stop if feasible
);
This configuration:
- Solves for up to 10 minutes
- Stops early if no improvement for 1 minute
- Stops immediately when a feasible solution is found
- Uses seed 42 for reproducible results
2 - Solver Factory
Create and run solvers with SolverFactory and Solver
The solver is executed via HTTP requests to the solver service.
SolveRequest
Build a request containing all solving inputs:
use solverforge_core::{
DomainObjectDto, ListAccessorDto, SolveRequest, TerminationConfig
};
use indexmap::IndexMap;
let request = SolveRequest::new(
domain, // IndexMap<String, DomainObjectDto>
constraints, // IndexMap<String, Vec<StreamComponent>>
wasm_base64, // Base64-encoded WASM module
"alloc".to_string(), // Memory allocator function
"dealloc".to_string(), // Memory deallocator function
list_accessor, // ListAccessorDto
problem_json, // JSON-serialized problem
);
Adding Configuration
let request = SolveRequest::new(/* ... */)
.with_environment_mode("REPRODUCIBLE")
.with_termination(
TerminationConfig::new()
.with_spent_limit("PT5M")
.with_best_score_feasible(true)
);
ListAccessorDto
Define WASM functions for list operations:
let list_accessor = ListAccessorDto::new(
"newList", // Create new list
"getItem", // Get item at index
"setItem", // Set item at index
"size", // Get list size
"append", // Append item
"insert", // Insert at index
"remove", // Remove at index
"dealloc", // Deallocate list
);
Sending Requests
Synchronous Solve
use reqwest::blocking::Client;
use solverforge_core::SolveResponse;
let client = Client::builder()
.timeout(std::time::Duration::from_secs(600))
.build()?;
let response: SolveResponse = client
.post(&format!("{}/solve", service.url()))
.header("Content-Type", "application/json")
.json(&request)
.send()?
.json()?;
println!("Score: {}", response.score);
println!("Solution: {}", response.solution);
Response Structure
pub struct SolveResponse {
pub score: String, // e.g., "0hard/-5soft"
pub solution: String, // JSON-serialized solution
pub stats: Option<SolverStats>,
}
SolveResponse Fields
| Field | Type | Description |
|---|
score | String | Final score (e.g., "0hard/-5soft") |
solution | String | JSON solution with assignments |
stats | Option<SolverStats> | Performance statistics |
Complete Example
use base64::{engine::general_purpose::STANDARD as BASE64, Engine as _};
use indexmap::IndexMap;
use reqwest::blocking::Client;
use solverforge_core::{
DomainObjectDto, ListAccessorDto, SolveRequest, SolveResponse,
StreamComponent, TerminationConfig, WasmFunction
};
use solverforge_service::{EmbeddedService, ServiceConfig};
use std::time::Duration;
// Start the solver service
let service = EmbeddedService::start(ServiceConfig::new())?;
// Build domain DTO from model
let domain = model.to_dto();
// Build constraints
let mut constraints = IndexMap::new();
constraints.insert("requiredSkill".to_string(), vec![
StreamComponent::for_each("Shift"),
StreamComponent::filter(WasmFunction::new("skillMismatch")),
StreamComponent::penalize("1hard/0soft"),
]);
// Encode WASM
let wasm_base64 = BASE64.encode(&wasm_bytes);
// Build list accessor
let list_accessor = ListAccessorDto::new(
"newList", "getItem", "setItem", "size",
"append", "insert", "remove", "dealloc"
);
// Problem data as JSON
let problem_json = r#"{"employees": [...], "shifts": [...]}"#;
// Build request
let request = SolveRequest::new(
domain,
constraints,
wasm_base64,
"alloc".to_string(),
"dealloc".to_string(),
list_accessor,
problem_json.to_string(),
)
.with_termination(TerminationConfig::new()
.with_move_count_limit(1000)
);
// Send request
let client = Client::builder()
.timeout(Duration::from_secs(120))
.build()?;
let response: SolveResponse = client
.post(&format!("{}/solve", service.url()))
.json(&request)
.send()?
.json()?;
// Parse result
println!("Score: {}", response.score);
// Parse solution JSON
let solution: serde_json::Value = serde_json::from_str(&response.solution)?;
let shifts = solution.get("shifts").unwrap().as_array().unwrap();
for shift in shifts {
let id = shift.get("id").unwrap();
let employee = shift.get("employee");
println!("Shift {}: {:?}", id, employee);
}
Error Handling
Check HTTP status and handle errors:
let response = client
.post(&format!("{}/solve", service.url()))
.json(&request)
.send()?;
if !response.status().is_success() {
let error_text = response.text()?;
eprintln!("Solver error: {}", error_text);
return Err(/* error */);
}
let result: SolveResponse = response.json()?;
Interpreting Scores
// Check if solution is feasible
if result.score.starts_with("0hard") || result.score.starts_with("0/") {
println!("Solution is feasible!");
} else {
println!("Solution has hard constraint violations");
}
// Parse score for detailed analysis
let score = solverforge_core::HardSoftScore::parse(&result.score)?;
println!("Hard: {}, Soft: {}", score.hard_score, score.soft_score);
println!("Feasible: {}", score.is_feasible());
3 - Score Types
Understand SimpleScore, HardSoftScore, HardMediumSoftScore, and BendableScore
Scores measure solution quality. SolverForge supports multiple score types.
Score Trait
All score types implement the Score trait:
pub trait Score {
fn is_feasible(&self) -> bool;
fn is_solution_initialized(&self) -> bool;
fn zero() -> Self;
fn negate(&self) -> Self;
fn add(&self, other: &Self) -> Self;
fn subtract(&self, other: &Self) -> Self;
}
Score Types
SimpleScore
Single dimension score:
use solverforge_core::SimpleScore;
let score = SimpleScore::of(-5);
println!("{}", score); // "-5"
HardSoftScore
Two dimensions - hard constraints and soft constraints:
use solverforge_core::HardSoftScore;
// Create scores
let score = HardSoftScore::of(-2, 10);
let zero = HardSoftScore::ZERO;
let one_hard = HardSoftScore::ONE_HARD;
let one_soft = HardSoftScore::ONE_SOFT;
// Convenience constructors
let hard_only = HardSoftScore::of_hard(-5);
let soft_only = HardSoftScore::of_soft(10);
// Access components
println!("Hard: {}, Soft: {}", score.hard_score, score.soft_score);
// Display format
println!("{}", score); // "-2hard/10soft"
HardMediumSoftScore
Three dimensions - hard, medium, and soft:
use solverforge_core::HardMediumSoftScore;
let score = HardMediumSoftScore::of(-1, 5, 10);
// Convenience constructors
let hard = HardMediumSoftScore::of_hard(-2);
let medium = HardMediumSoftScore::of_medium(3);
let soft = HardMediumSoftScore::of_soft(10);
// Display format
println!("{}", score); // "-1hard/5medium/10soft"
BendableScore
Configurable number of hard and soft levels:
use solverforge_core::BendableScore;
// 2 hard levels, 3 soft levels
let score = BendableScore::of(
vec![-1, 0], // hard scores
vec![5, 10, 2] // soft scores
);
Decimal Variants
For precise decimal arithmetic:
SimpleDecimalScoreHardSoftDecimalScoreHardMediumSoftDecimalScoreBendableDecimalScore
use solverforge_core::HardSoftDecimalScore;
use rust_decimal::Decimal;
let score = HardSoftDecimalScore::of(
Decimal::from(-2),
Decimal::new(105, 1) // 10.5
);
Feasibility
A solution is feasible when all hard constraints are satisfied:
use solverforge_core::HardSoftScore;
let feasible = HardSoftScore::of(0, -100);
let infeasible = HardSoftScore::of(-1, 100);
assert!(feasible.is_feasible()); // true - hard >= 0
assert!(!infeasible.is_feasible()); // false - hard < 0
Score Comparison
Scores are compared lexicographically (hard first, then soft):
use solverforge_core::HardSoftScore;
// Hard score takes priority
assert!(HardSoftScore::of(0, 0) > HardSoftScore::of(-1, 1000));
// Same hard: compare soft
assert!(HardSoftScore::of(0, 10) > HardSoftScore::of(0, 5));
assert!(HardSoftScore::of(0, -5) > HardSoftScore::of(0, -10));
Score Arithmetic
use solverforge_core::HardSoftScore;
let a = HardSoftScore::of(-2, 10);
let b = HardSoftScore::of(-1, 5);
// Addition
let sum = a + b; // -3hard/15soft
// Subtraction
let diff = a - b; // -1hard/5soft
// Negation
let neg = -a; // 2hard/-10soft
Parsing Scores
Parse score strings returned by the solver:
use solverforge_core::HardSoftScore;
// With labels
let score = HardSoftScore::parse("0hard/-5soft")?;
// Without labels
let score = HardSoftScore::parse("-2/-10")?;
// Parse and check feasibility
let score = HardSoftScore::parse(&response.score)?;
if score.is_feasible() {
println!("Solution satisfies all hard constraints");
}
Weight Strings
Penalty/reward weights use score format strings:
// HardSoftScore weights
StreamComponent::penalize("1hard/0soft") // 1 hard point per match
StreamComponent::penalize("0hard/1soft") // 1 soft point per match
StreamComponent::penalize("1hard/5soft") // Both hard and soft
// SimpleScore weights
StreamComponent::penalize("1")
// HardMediumSoftScore weights
StreamComponent::penalize("0hard/1medium/0soft")
Score Type Summary
| Type | Dimensions | Format | Feasible When |
|---|
SimpleScore | 1 | -5 | score >= 0 |
HardSoftScore | 2 | -2hard/10soft | hard >= 0 |
HardMediumSoftScore | 3 | -1hard/5medium/10soft | hard >= 0 |
BendableScore | N | [-1, 0]/[5, 10, 2] | all hard >= 0 |
Constants
use solverforge_core::HardSoftScore;
HardSoftScore::ZERO // 0hard/0soft
HardSoftScore::ONE_HARD // 1hard/0soft
HardSoftScore::ONE_SOFT // 0hard/1soft