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

Return to the regular view of this page.

Domain Modeling

Define your planning domain with DomainModel, classes, and fields

This section covers how to define your planning domain using SolverForge’s domain modeling API.

In This Section

  • Domain Model - Build domain models with DomainModel, DomainClass, and FieldDescriptor
  • Planning Annotations - Configure planning behavior with PlanningAnnotation types
  • Field Types - Supported field types including primitives, objects, collections, and scores

Overview

A domain model describes the structure of your planning problem:

use solverforge_core::domain::{
    DomainClass, DomainModel, FieldDescriptor, FieldType,
    PlanningAnnotation, PrimitiveType, ScoreType,
};

let model = DomainModel::builder()
    .add_class(
        DomainClass::new("Shift")
            .with_annotation(PlanningAnnotation::PlanningEntity)
            .with_field(/* ... */)
    )
    .add_class(
        DomainClass::new("Schedule")
            .with_annotation(PlanningAnnotation::PlanningSolution)
            .with_field(/* ... */)
    )
    .build();

Key Concepts

ConceptPurposeAnnotation
Planning EntityObject modified by solver@PlanningEntity
Planning VariableField assigned by solver@PlanningVariable
Planning SolutionContainer for all data@PlanningSolution
Problem FactRead-only data(no annotation needed)
Value Range ProviderSource of variable values@ValueRangeProvider

Model Validation

Use build_validated() to catch configuration errors early:

let model = DomainModel::builder()
    .add_class(/* ... */)
    .build_validated()?;  // Returns SolverForgeError on invalid model

Validation checks:

  • Solution class exists with @PlanningSolution
  • At least one entity class with @PlanningEntity
  • Each entity has at least one @PlanningVariable
  • Solution class has a @PlanningScore field

1 - Domain Model

Build domain models with DomainModel, DomainClass, and FieldDescriptor

The DomainModel is the central data structure that describes your planning domain.

Building a Model

Use the builder pattern to construct a domain model:

use solverforge_core::domain::{
    DomainClass, DomainModel, FieldDescriptor, FieldType,
    PlanningAnnotation, PrimitiveType, ScoreType,
};

let model = DomainModel::builder()
    .add_class(DomainClass::new("Employee")/* ... */)
    .add_class(DomainClass::new("Shift")/* ... */)
    .add_class(DomainClass::new("Schedule")/* ... */)
    .build();

DomainClass

Each class in your domain is defined with DomainClass:

let shift = DomainClass::new("Shift")
    .with_annotation(PlanningAnnotation::PlanningEntity)
    .with_field(
        FieldDescriptor::new("id", FieldType::Primitive(PrimitiveType::String))
            .with_planning_annotation(PlanningAnnotation::PlanningId),
    )
    .with_field(
        FieldDescriptor::new("employee", FieldType::object("Employee"))
            .with_planning_annotation(
                PlanningAnnotation::planning_variable(vec!["employees".to_string()])
            ),
    )
    .with_field(FieldDescriptor::new(
        "start",
        FieldType::Primitive(PrimitiveType::DateTime),
    ))
    .with_field(FieldDescriptor::new(
        "end",
        FieldType::Primitive(PrimitiveType::DateTime),
    ));

Class Methods

MethodPurpose
DomainClass::new(name)Create a new class
.with_annotation(ann)Add a class-level annotation
.with_field(field)Add a field
.is_planning_entity()Check if @PlanningEntity
.is_planning_solution()Check if @PlanningSolution
.get_planning_variables()Iterator over planning variable fields

FieldDescriptor

Each field is defined with FieldDescriptor:

let employee_field = FieldDescriptor::new("employee", FieldType::object("Employee"))
    .with_planning_annotation(PlanningAnnotation::planning_variable(vec!["employees"]))
    .with_accessor(DomainAccessor::new("getEmployee", "setEmployee"));

Field Methods

MethodPurpose
FieldDescriptor::new(name, type)Create a new field
.with_planning_annotation(ann)Add a planning annotation
.with_shadow_annotation(ann)Add a shadow variable annotation
.with_accessor(acc)Override default accessor names
.is_planning_variable()Check if @PlanningVariable

Complete Example

let model = DomainModel::builder()
    // Problem fact: Employee (not modified by solver)
    .add_class(
        DomainClass::new("Employee")
            .with_field(
                FieldDescriptor::new("name", FieldType::Primitive(PrimitiveType::String))
                    .with_planning_annotation(PlanningAnnotation::PlanningId),
            )
            .with_field(FieldDescriptor::new(
                "skills",
                FieldType::list(FieldType::Primitive(PrimitiveType::String)),
            )),
    )
    // Planning entity: Shift (employee assignment decided by solver)
    .add_class(
        DomainClass::new("Shift")
            .with_annotation(PlanningAnnotation::PlanningEntity)
            .with_field(
                FieldDescriptor::new("id", FieldType::Primitive(PrimitiveType::String))
                    .with_planning_annotation(PlanningAnnotation::PlanningId),
            )
            .with_field(
                FieldDescriptor::new("employee", FieldType::object("Employee"))
                    .with_planning_annotation(
                        PlanningAnnotation::planning_variable(vec!["employees".to_string()])
                    ),
            )
            .with_field(FieldDescriptor::new(
                "requiredSkill",
                FieldType::Primitive(PrimitiveType::String),
            )),
    )
    // Planning solution: Schedule (container)
    .add_class(
        DomainClass::new("Schedule")
            .with_annotation(PlanningAnnotation::PlanningSolution)
            .with_field(
                FieldDescriptor::new("employees", FieldType::list(FieldType::object("Employee")))
                    .with_planning_annotation(PlanningAnnotation::ProblemFactCollectionProperty)
                    .with_planning_annotation(PlanningAnnotation::value_range_provider("employees")),
            )
            .with_field(
                FieldDescriptor::new("shifts", FieldType::list(FieldType::object("Shift")))
                    .with_planning_annotation(PlanningAnnotation::PlanningEntityCollectionProperty),
            )
            .with_field(
                FieldDescriptor::new("score", FieldType::Score(ScoreType::HardSoft))
                    .with_planning_annotation(PlanningAnnotation::planning_score()),
            ),
    )
    .build_validated()?;

Converting to DTO

Convert the model to a DTO for the solver request:

let domain_dto = model.to_dto();

The to_dto() method:

  • Generates accessor names matching WASM exports: get_{Class}_{field}, set_{Class}_{field}
  • Adds setters for planning variables and collection properties
  • Adds mapper for the solution class (parseSchedule, scheduleString)

DomainModel Methods

MethodPurpose
DomainModel::builder()Create a builder
.add_class(class)Add a class
.build()Build without validation
.build_validated()Build with validation
model.get_class(name)Get a class by name
model.get_solution_class()Get the solution class
model.get_entity_classes()Iterator over entity classes
model.to_dto()Convert to solver DTO
model.validate()Validate the model

2 - Planning Annotations

Configure planning behavior with PlanningAnnotation types

Annotations configure how the solver interprets your domain model.

Class Annotations

PlanningEntity

Marks a class as a planning entity - an object whose planning variables are assigned by the solver.

DomainClass::new("Shift")
    .with_annotation(PlanningAnnotation::PlanningEntity)

PlanningSolution

Marks a class as the planning solution - the container for all problem facts, entities, and the score.

DomainClass::new("Schedule")
    .with_annotation(PlanningAnnotation::PlanningSolution)

Field Annotations

PlanningId

Marks a field as the unique identifier for instances of the class.

FieldDescriptor::new("id", FieldType::Primitive(PrimitiveType::String))
    .with_planning_annotation(PlanningAnnotation::PlanningId)

PlanningVariable

Marks a field whose value is determined by the solver. Must reference value range providers.

// Basic planning variable
FieldDescriptor::new("employee", FieldType::object("Employee"))
    .with_planning_annotation(
        PlanningAnnotation::planning_variable(vec!["employees".to_string()])
    )

// Planning variable that allows null (unassigned)
FieldDescriptor::new("room", FieldType::object("Room"))
    .with_planning_annotation(
        PlanningAnnotation::planning_variable_unassigned(vec!["rooms".to_string()])
    )

Parameters:

  • value_range_provider_refs: List of value range provider IDs that supply valid values
  • allows_unassigned: If true, the variable can remain unassigned (null)

PlanningListVariable

Marks a list field where the solver assigns which elements belong to the list.

FieldDescriptor::new("visits", FieldType::list(FieldType::object("Visit")))
    .with_planning_annotation(
        PlanningAnnotation::planning_list_variable(vec!["visits".to_string()])
    )

PlanningScore

Marks the score field on the solution class.

// Standard score
FieldDescriptor::new("score", FieldType::Score(ScoreType::HardSoft))
    .with_planning_annotation(PlanningAnnotation::planning_score())

// Bendable score with specific levels
FieldDescriptor::new("score", FieldType::Score(ScoreType::Bendable { hard_levels: 2, soft_levels: 3 }))
    .with_planning_annotation(PlanningAnnotation::planning_score_bendable(2, 3))

ValueRangeProvider

Marks a field that provides valid values for planning variables.

FieldDescriptor::new("employees", FieldType::list(FieldType::object("Employee")))
    .with_planning_annotation(PlanningAnnotation::value_range_provider("employees"))

The ID ("employees") must match the value_range_provider_refs in corresponding planning variables.

ProblemFactCollectionProperty

Marks a collection of problem facts (read-only data) on the solution class.

FieldDescriptor::new("employees", FieldType::list(FieldType::object("Employee")))
    .with_planning_annotation(PlanningAnnotation::ProblemFactCollectionProperty)

PlanningEntityCollectionProperty

Marks a collection of planning entities on the solution class.

FieldDescriptor::new("shifts", FieldType::list(FieldType::object("Shift")))
    .with_planning_annotation(PlanningAnnotation::PlanningEntityCollectionProperty)

PlanningPin

Marks a boolean field that pins an entity’s assignment (prevents the solver from changing it).

FieldDescriptor::new("pinned", FieldType::Primitive(PrimitiveType::Bool))
    .with_planning_annotation(PlanningAnnotation::PlanningPin)

InverseRelationShadowVariable

Marks a shadow variable that tracks the inverse of another planning variable.

FieldDescriptor::new("vehicle", FieldType::object("Vehicle"))
    .with_planning_annotation(PlanningAnnotation::inverse_relation_shadow("visits"))

Annotation Summary

AnnotationTargetPurpose
PlanningEntityClassMark as planning entity
PlanningSolutionClassMark as solution container
PlanningIdFieldUnique identifier
PlanningVariableFieldSolver-assigned value
PlanningListVariableFieldSolver-assigned list
PlanningScoreFieldScore field on solution
ValueRangeProviderFieldSource of valid values
ProblemFactCollectionPropertyFieldProblem fact collection
PlanningEntityCollectionPropertyFieldEntity collection
PlanningPinFieldPin entity assignment
InverseRelationShadowVariableFieldShadow inverse relation

Helper Methods

use solverforge_core::domain::PlanningAnnotation;

// Create planning variable
PlanningAnnotation::planning_variable(vec!["rooms".to_string()])
PlanningAnnotation::planning_variable_unassigned(vec!["slots".to_string()])

// Create list variable
PlanningAnnotation::planning_list_variable(vec!["tasks".to_string()])

// Create score annotation
PlanningAnnotation::planning_score()
PlanningAnnotation::planning_score_bendable(2, 3)

// Create value range provider
PlanningAnnotation::value_range_provider("timeslots")

// Create shadow variable
PlanningAnnotation::inverse_relation_shadow("visits")

Multiple Annotations

A field can have multiple annotations:

FieldDescriptor::new("employees", FieldType::list(FieldType::object("Employee")))
    .with_planning_annotation(PlanningAnnotation::ProblemFactCollectionProperty)
    .with_planning_annotation(PlanningAnnotation::value_range_provider("employees"))

3 - Field Types

Supported field types including primitives, objects, collections, and scores

SolverForge supports various field types for domain modeling.

Primitive Types

Basic value types:

use solverforge_core::domain::{FieldType, PrimitiveType};

// Boolean
FieldType::Primitive(PrimitiveType::Bool)

// Integers
FieldType::Primitive(PrimitiveType::Int)    // 32-bit
FieldType::Primitive(PrimitiveType::Long)   // 64-bit

// Floating point
FieldType::Primitive(PrimitiveType::Float)  // 32-bit
FieldType::Primitive(PrimitiveType::Double) // 64-bit

// String
FieldType::Primitive(PrimitiveType::String)

// Date/Time (stored as epoch values)
FieldType::Primitive(PrimitiveType::Date)     // LocalDate - epoch day (i64)
FieldType::Primitive(PrimitiveType::DateTime) // LocalDateTime - epoch second (i64)

Date and Time Handling

Dates and times are stored as integers:

  • Date (LocalDate): Epoch day (days since 1970-01-01)
  • DateTime (LocalDateTime): Epoch second (seconds since 1970-01-01T00:00:00)
// Field definition
FieldDescriptor::new("start", FieldType::Primitive(PrimitiveType::DateTime))

// JSON representation
// "start": "2025-01-15T08:00:00" is converted to epoch seconds

Object Types

References to other domain classes:

// Reference to Employee class
FieldType::object("Employee")

// Usage in field descriptor
FieldDescriptor::new("employee", FieldType::object("Employee"))

Collection Types

List

Ordered collection (most common):

// List of strings
FieldType::list(FieldType::Primitive(PrimitiveType::String))

// List of objects
FieldType::list(FieldType::object("Shift"))

Array

Fixed-size array (similar to List in behavior):

FieldType::array(FieldType::Primitive(PrimitiveType::Int))

Set

Unordered unique elements:

FieldType::set(FieldType::object("Skill"))

Map

Key-value mapping:

FieldType::map(
    FieldType::Primitive(PrimitiveType::String),  // Key type
    FieldType::object("Employee")                  // Value type
)

Score Types

Score types for the planning solution:

use solverforge_core::domain::ScoreType;

// Single dimension
FieldType::Score(ScoreType::Simple)
FieldType::Score(ScoreType::SimpleDecimal)

// Two dimensions (hard/soft)
FieldType::Score(ScoreType::HardSoft)
FieldType::Score(ScoreType::HardSoftDecimal)

// Three dimensions (hard/medium/soft)
FieldType::Score(ScoreType::HardMediumSoft)
FieldType::Score(ScoreType::HardMediumSoftDecimal)

// Configurable dimensions
FieldType::Score(ScoreType::Bendable { hard_levels: 2, soft_levels: 3 })
FieldType::Score(ScoreType::BendableDecimal { hard_levels: 2, soft_levels: 3 })

Type String Conversion

FieldType::to_type_string() converts to Java-compatible type strings:

Rust TypeJava Type String
PrimitiveType::Boolboolean
PrimitiveType::Intint
PrimitiveType::Longlong
PrimitiveType::Floatfloat
PrimitiveType::Doubledouble
PrimitiveType::StringString
PrimitiveType::DateLocalDate
PrimitiveType::DateTimeLocalDateTime
FieldType::object("Foo")Foo
FieldType::list(...)...[]
ScoreType::HardSoftHardSoftScore

Examples

Employee Class

DomainClass::new("Employee")
    .with_field(
        FieldDescriptor::new("name", FieldType::Primitive(PrimitiveType::String))
            .with_planning_annotation(PlanningAnnotation::PlanningId),
    )
    .with_field(FieldDescriptor::new(
        "skills",
        FieldType::list(FieldType::Primitive(PrimitiveType::String)),
    ))
    .with_field(FieldDescriptor::new(
        "unavailableDates",
        FieldType::list(FieldType::Primitive(PrimitiveType::Date)),
    ))

Shift Class

DomainClass::new("Shift")
    .with_annotation(PlanningAnnotation::PlanningEntity)
    .with_field(
        FieldDescriptor::new("id", FieldType::Primitive(PrimitiveType::String))
            .with_planning_annotation(PlanningAnnotation::PlanningId),
    )
    .with_field(
        FieldDescriptor::new("employee", FieldType::object("Employee"))
            .with_planning_annotation(PlanningAnnotation::planning_variable(vec!["employees"])),
    )
    .with_field(FieldDescriptor::new(
        "start",
        FieldType::Primitive(PrimitiveType::DateTime),
    ))
    .with_field(FieldDescriptor::new(
        "end",
        FieldType::Primitive(PrimitiveType::DateTime),
    ))
    .with_field(FieldDescriptor::new(
        "requiredSkill",
        FieldType::Primitive(PrimitiveType::String),
    ))

Schedule Class

DomainClass::new("Schedule")
    .with_annotation(PlanningAnnotation::PlanningSolution)
    .with_field(
        FieldDescriptor::new("employees", FieldType::list(FieldType::object("Employee")))
            .with_planning_annotation(PlanningAnnotation::ProblemFactCollectionProperty)
            .with_planning_annotation(PlanningAnnotation::value_range_provider("employees")),
    )
    .with_field(
        FieldDescriptor::new("shifts", FieldType::list(FieldType::object("Shift")))
            .with_planning_annotation(PlanningAnnotation::PlanningEntityCollectionProperty),
    )
    .with_field(
        FieldDescriptor::new("score", FieldType::Score(ScoreType::HardSoft))
            .with_planning_annotation(PlanningAnnotation::planning_score()),
    )

Collection Checks

let list_type = FieldType::list(FieldType::object("Shift"));
assert!(list_type.is_collection()); // true

let object_type = FieldType::object("Employee");
assert!(!object_type.is_collection()); // false