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:
- Satisfies all hard constraints (feasibility)
- Optimizes soft constraints (quality)
Example: Employee Scheduling
| Concept | Example |
|---|
| Planning Entity | Shift |
| Planning Variable | Shift.employee |
| Problem Fact | Employee, Skill |
| Hard Constraint | Employee must have required skill |
| Soft Constraint | Balance assignments fairly |
| Score | HardSoftScore (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:
| Module | Purpose |
|---|
domain | Define planning entities and fields |
constraints | Build constraint streams |
wasm | Generate WASM modules |
solver | Configure solver and send requests |
score | Score types (HardSoft, Bendable, etc.) |
timefold-wasm-service (Java)
The solver service handles:
| Component | Purpose |
|---|
| Chicory WASM Runtime | Execute constraint predicates |
| Dynamic Class Generation | Create Java classes from domain DTOs |
| Timefold Solver | Run optimization algorithms |
| Host Functions | Bridge 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:
| Type | Alignment | Size |
|---|
| int, float, pointer | 4 bytes | 4 bytes |
| long, double, DateTime | 8 bytes | 8 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:
| Function | Purpose |
|---|
string_equals | Compare two strings |
list_contains | Check if list contains element |
ranges_overlap | Check if time ranges overlap |
hround | Round 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 Type | Levels | Example |
|---|
SimpleScore | 1 | -5 |
HardSoftScore | 2 | 0hard/-10soft |
HardMediumSoftScore | 3 | 0hard/0medium/-5soft |
BendableScore | N | Configurable 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
- Initial Solution: Start with planning variables unassigned or randomly assigned
- Move Selection: Choose a move (e.g., assign employee A to shift 1)
- Score Calculation: Evaluate all constraints after the move
- Move Acceptance: Accept or reject based on score improvement
- Termination: Stop when time limit, score limit, or other condition is met
Constraint Streams
Constraints are expressed as pipelines that:
- Select entities:
for_each("Shift") - Filter matches:
filter(predicate) - Join with other entities:
join("Employee") - Group for aggregation:
group_by(key, collector) - 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