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

Return to the regular view of this page.

Concepts

Understand the core concepts behind SolverForge

This section covers the foundational concepts you need to understand when working with SolverForge.

In This Section

Overview

SolverForge solves constraint satisfaction and optimization problems (CSPs). Given:

  • A set of planning entities with planning variables to assign
  • A set of constraints that define valid and preferred solutions
  • An objective function (score) to optimize

The solver searches for an assignment of values to planning variables that:

  1. Satisfies all hard constraints (feasibility)
  2. Optimizes soft constraints (quality)

Example: Employee Scheduling

ConceptExample
Planning EntityShift
Planning VariableShift.employee
Problem FactEmployee, Skill
Hard ConstraintEmployee must have required skill
Soft ConstraintBalance assignments fairly
ScoreHardSoftScore (e.g., 0hard/-5soft)

The solver tries different employee assignments for each shift, evaluating constraints until it finds a feasible, high-quality solution.

1 - Architecture

How SolverForge uses WASM and HTTP to solve constraints

SolverForge uses a layered architecture that separates constraint definition (Rust) from solving execution (Java/Timefold).

Overview

┌─────────────────────────────────────────────────────────────────┐
│                    Your Application (Rust)                       │
│  • Define domain model                                          │
│  • Build constraints                                            │
│  • Generate WASM predicates                                     │
└─────────────────────────────────────────────────────────────────┘
                              │
                         HTTP/JSON
                              │
                              ▼
┌─────────────────────────────────────────────────────────────────┐
│               timefold-wasm-service (Java)                       │
│  • Execute WASM predicates via Chicory runtime                  │
│  • Run Timefold solver algorithms                               │
│  • Return optimized solution                                    │
└─────────────────────────────────────────────────────────────────┘

Why This Architecture?

WASM for Portability

Constraint predicates are compiled to WebAssembly, which:

  • Runs safely in a sandboxed environment
  • Executes at near-native speed
  • Works across different language runtimes

HTTP for Simplicity

Using HTTP/JSON instead of JNI provides:

  • Clean separation between Rust and Java
  • Easy debugging (inspect JSON requests/responses)
  • Language-agnostic interface for future bindings

Components

solverforge-core (Rust)

The core library provides:

ModulePurpose
domainDefine planning entities and fields
constraintsBuild constraint streams
wasmGenerate WASM modules
solverConfigure solver and send requests
scoreScore types (HardSoft, Bendable, etc.)

timefold-wasm-service (Java)

The solver service handles:

ComponentPurpose
Chicory WASM RuntimeExecute constraint predicates
Dynamic Class GenerationCreate Java classes from domain DTOs
Timefold SolverRun optimization algorithms
Host FunctionsBridge WASM calls to Java operations

Request Flow

1. Build Domain Model      → DomainModel with annotations
2. Create Constraints      → Constraint streams (forEach, filter, penalize)
3. Generate WASM           → Predicates compiled to WebAssembly
4. Build SolveRequest      → JSON payload with domain + constraints + WASM + problem
5. Send HTTP POST          → /solve endpoint
6. Solver Executes         → Timefold evaluates constraints via WASM
7. Return Solution         → JSON response with score and assignments

WASM Memory Layout

Domain objects are stored in WASM linear memory with proper alignment:

TypeAlignmentSize
int, float, pointer4 bytes4 bytes
long, double, DateTime8 bytes8 bytes

Example Shift layout:

Field            Offset  Size  Type
───────────────────────────────────
id               0       4     String (pointer)
employee         4       4     Employee (pointer)
location         8       4     String (pointer)
[padding]        12      4     (align for DateTime)
start            16      8     LocalDateTime
end              24      8     LocalDateTime
requiredSkill    32      4     String (pointer)
───────────────────────────────────
Total: 40 bytes

Both Rust (WASM generation) and Java (runtime) use identical alignment rules.

Host Functions

WASM predicates can call host functions for operations that require Java:

FunctionPurpose
string_equalsCompare two strings
list_containsCheck if list contains element
ranges_overlapCheck if time ranges overlap
hroundRound float to integer

These are injected into the WASM module via HostFunctionRegistry.

2 - Constraint Satisfaction

Core concepts of constraint satisfaction and optimization problems

SolverForge solves constraint satisfaction and optimization problems (CSPs). This page explains the core concepts.

Problem Structure

Every planning problem has:

Planning Entities

Objects that the solver modifies. Each entity has one or more planning variables that the solver assigns.

// Shift is a planning entity
DomainClass::new("Shift")
    .with_annotation(PlanningAnnotation::PlanningEntity)
    .with_field(
        // employee is a planning variable - solver decides the value
        FieldDescriptor::new("employee", FieldType::object("Employee"))
            .with_planning_annotation(PlanningAnnotation::planning_variable(vec!["employees"]))
    )

Problem Facts

Objects that provide data but are not modified by the solver.

// Employee is a problem fact - solver reads but doesn't change it
DomainClass::new("Employee")
    .with_field(FieldDescriptor::new("name", FieldType::Primitive(PrimitiveType::String)))
    .with_field(FieldDescriptor::new("skills", FieldType::list(FieldType::Primitive(PrimitiveType::String))))

Planning Solution

A container that holds all entities and problem facts, plus the score.

DomainClass::new("Schedule")
    .with_annotation(PlanningAnnotation::PlanningSolution)
    .with_field(/* employees - problem facts */)
    .with_field(/* shifts - planning entities */)
    .with_field(/* score - optimization result */)

Constraints

Constraints define what makes a solution valid and good.

Hard Constraints

Must be satisfied for a solution to be feasible. Violations result in negative hard score.

// Every shift must have an employee with the required skill
StreamComponent::for_each("Shift"),
StreamComponent::filter(WasmFunction::new("skillMismatch")),
StreamComponent::penalize("1hard/0soft"),

Soft Constraints

Should be satisfied for a better solution. Violations result in negative soft score.

// Prefer balanced shift distribution
StreamComponent::for_each("Shift"),
StreamComponent::group_by(/* ... */),
StreamComponent::penalize("0hard/1soft"),

Score Types

The score measures solution quality. SolverForge supports multiple score types:

Score TypeLevelsExample
SimpleScore1-5
HardSoftScore20hard/-10soft
HardMediumSoftScore30hard/0medium/-5soft
BendableScoreNConfigurable levels

Score Interpretation

  • Hard score = 0: All hard constraints satisfied (feasible)
  • Hard score < 0: Hard constraints violated (infeasible)
  • Soft score: Higher is better (less negative = fewer soft violations)

Example: 0hard/-5soft is feasible but has 5 soft constraint points violated.

Solving Process

  1. Initial Solution: Start with planning variables unassigned or randomly assigned
  2. Move Selection: Choose a move (e.g., assign employee A to shift 1)
  3. Score Calculation: Evaluate all constraints after the move
  4. Move Acceptance: Accept or reject based on score improvement
  5. Termination: Stop when time limit, score limit, or other condition is met

Constraint Streams

Constraints are expressed as pipelines that:

  1. Select entities: for_each("Shift")
  2. Filter matches: filter(predicate)
  3. Join with other entities: join("Employee")
  4. Group for aggregation: group_by(key, collector)
  5. Penalize/Reward: penalize("1hard/0soft")

Example constraint pipeline:

forEach(Shift)
  → filter(unassignedEmployee)
  → penalize(1hard)

This penalizes every shift that has no employee assigned.

Next Steps