This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

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:

  1. Configure - Set termination conditions, environment mode, and threading
  2. Build Request - Combine domain, constraints, WASM, and problem data
  3. Execute - Send to solver service and receive solution
  4. 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:

ConditionDescription
spent_limitMaximum solving time (e.g., "PT5M")
best_score_feasibleStop when feasible
best_score_limitStop at target score
move_count_limitMaximum moves
step_count_limitMaximum 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

MethodDescription
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

ModeDescription
ReproducibleSame seed = same solution (default)
NonReproducibleRandom behavior each run
NoAssertMinimal validation (fastest)
PhaseAssertValidate after each phase
StepAssertValidate after each step
FullAssertMaximum validation (slowest)
TrackedFullAssertFull validation with tracking

Move Thread Count

OptionDescription
MoveThreadCount::AutoUse available CPUs
MoveThreadCount::NoneSingle-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

MethodDescription
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:

  1. Solves for up to 10 minutes
  2. Stops early if no improvement for 1 minute
  3. Stops immediately when a feasible solution is found
  4. 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

FieldTypeDescription
scoreStringFinal score (e.g., "0hard/-5soft")
solutionStringJSON solution with assignments
statsOption<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:

  • SimpleDecimalScore
  • HardSoftDecimalScore
  • HardMediumSoftDecimalScore
  • BendableDecimalScore
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

TypeDimensionsFormatFeasible When
SimpleScore1-5score >= 0
HardSoftScore2-2hard/10softhard >= 0
HardMediumSoftScore3-1hard/5medium/10softhard >= 0
BendableScoreN[-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