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 planning solutions, entities, and problem facts using Rust derive macros.

SolverForge uses derive macros to turn your Rust structs into a planning domain. There are three key concepts:

ConceptMacroPurpose
Planning Solution#[planning_solution]The top-level container — holds entities, facts, and the score
Planning Entity#[planning_entity]Something the solver changes (assigns variables to)
Problem Fact#[problem_fact]Immutable input data the solver reads but doesn’t modify
Planning Solution
├── problem_fact_collection  → Vec<ProblemFact>   (inputs)
├── planning_entity_collection → Vec<Entity>      (solver changes these)
├── value_range_provider     → Vec<Value>          (possible values)
└── planning_score           → Option<ScoreType>   (current quality)

How It Works

  1. Annotate your structs with the appropriate derive macros
  2. Mark fields with attribute macros to tell the solver their role
  3. The macros generate trait implementations that the solver uses at runtime

The derive macros generate implementations of PlanningSolution, PlanningEntity, and ProblemFact traits automatically — you never implement these traits by hand.

Sections

See Also

1 - Planning Solutions

The top-level container that holds entities, problem facts, value ranges, and the score.

A planning solution is the root struct that represents your entire problem and its current solution state. It holds all entities, problem facts, available values, and the score.

The #[planning_solution] Macro

use solverforge::prelude::*;

#[planning_solution(constraints = "crate::constraints::define_constraints")]
pub struct Schedule {
    #[problem_fact_collection]
    #[value_range_provider]
    pub employees: Vec<Employee>,

    #[problem_fact_collection]
    pub availability: Vec<Availability>,

    #[planning_entity_collection]
    pub shifts: Vec<Shift>,

    #[planning_score]
    pub score: Option<HardSoftScore>,
}

The constraints parameter specifies the module path to the constraint provider function.

Field Attributes

#[planning_entity_collection]

Marks a Vec<T> field containing planning entities. The solver iterates these to find variables to change.

#[planning_entity_collection]
pub shifts: Vec<Shift>,

#[problem_fact_collection]

Marks a Vec<T> field containing immutable problem facts. Used by constraints but never modified by the solver.

#[problem_fact_collection]
pub employees: Vec<Employee>,

#[value_range_provider]

Marks a collection as providing possible values for planning variables. Usually combined with #[problem_fact_collection].

#[problem_fact_collection]
#[value_range_provider]
pub timeslots: Vec<Timeslot>,

The solver draws from this collection when trying assignments for any #[planning_variable] field whose type matches.

#[planning_score]

Marks the field that holds the current solution quality. Must be Option<ScoreType>.

#[planning_score]
pub score: Option<HardSoftScore>,

Supported score types: SoftScore, HardSoftScore, HardMediumSoftScore, HardSoftDecimalScore, BendableScore.

Requirements

  • Must derive Clone and Debug (added automatically by the macro)
  • Must have exactly one #[planning_score] field
  • Must have at least one #[planning_entity_collection] field
  • Must have at least one #[value_range_provider] field

See Also

2 - Planning Entities

Structs with planning variables that the solver assigns during optimization.

A planning entity is a struct that contains one or more planning variables — fields the solver changes to find a good solution. In employee scheduling, a Shift is an entity because the solver assigns an Employee to each shift.

The #[planning_entity] Macro

use solverforge::prelude::*;

#[planning_entity]
#[derive(Clone, Debug)]
pub struct Shift {
    #[planning_id]
    pub id: i64,
    pub required_skill: String,
    pub timeslot: Timeslot,
    #[planning_variable(allows_unassigned = true)]
    pub employee: Option<Employee>,
}

Field Attributes

#[planning_id]

Uniquely identifies the entity. Required on every planning entity.

#[planning_id]
pub id: i64,

#[planning_variable]

Marks a field as a planning variable — the solver assigns values to this field.

// Nullable variable (solver can leave unassigned)
#[planning_variable(allows_unassigned = true)]
pub employee: Option<Employee>,

// Non-nullable variable (solver must assign a value)
#[planning_variable]
pub timeslot: Timeslot,

allows_unassigned = true: The variable can be None, meaning the solver may leave some entities unassigned. Use this when not every entity must be assigned (e.g., optional shifts). The field type must be Option<T>.

#[planning_pin]

Prevents the solver from changing an entity’s variables. Useful for pre-assigned or locked entities.

#[planning_pin]
pub pinned: bool,

When pinned is true, the solver treats the entity as immovable.

Shadow Variables

Shadow variables are automatically calculated from genuine planning variables. They are derived values that the solver maintains — you never assign them directly.

#[inverse_relation_shadow_variable]

Automatically tracks which entities are assigned to a value. For example, tracking which shifts an employee has:

#[planning_entity]
#[derive(Clone, Debug)]
pub struct Employee {
    #[planning_id]
    pub id: i64,
    #[inverse_relation_shadow_variable(source_variable = "employee")]
    pub assigned_shifts: Vec<Shift>,
}

#[previous_element_shadow_variable]

For list variables — automatically tracks the previous element in the list.

#[previous_element_shadow_variable(source_variable = "stops")]
pub previous_stop: Option<Stop>,

#[next_element_shadow_variable]

For list variables — automatically tracks the next element in the list.

#[next_element_shadow_variable(source_variable = "stops")]
pub next_stop: Option<Stop>,

Requirements

  • Must derive Clone and Debug
  • Must have exactly one #[planning_id] field
  • Must have at least one #[planning_variable] or list variable field

See Also

3 - Problem Facts

Immutable input data that constraints reference but the solver doesn’t modify.

A problem fact is an immutable struct that represents input data. The solver reads problem facts when evaluating constraints but never modifies them. Examples include employees, timeslots, rooms, and skills.

The #[problem_fact] Macro

use solverforge::prelude::*;

#[problem_fact]
#[derive(Clone, Debug)]
pub struct Employee {
    #[planning_id]
    pub id: i64,
    pub name: String,
    pub skills: Vec<String>,
}

#[problem_fact]
#[derive(Clone, Debug)]
pub struct Timeslot {
    #[planning_id]
    pub id: i64,
    pub day_of_week: String,
    pub start_time: String,
    pub end_time: String,
}

Field Attributes

#[planning_id]

Uniquely identifies the problem fact. Required.

#[planning_id]
pub id: i64,

When to Use Problem Facts vs Planning Entities

Problem FactPlanning Entity
Modified by solver?NoYes
Has planning variables?NoYes
ExampleEmployee, Room, TimeslotShift, Lesson, Visit
RoleInput data / possible valuesThings being assigned

A common pattern: problem facts serve as the value range for planning variables.

#[planning_solution]
pub struct Schedule {
    #[problem_fact_collection]
    #[value_range_provider]       // Employees are possible values...
    pub employees: Vec<Employee>,

    #[planning_entity_collection]
    pub shifts: Vec<Shift>,       // ...assigned to shift.employee
}

Requirements

  • Must derive Clone and Debug
  • Must have exactly one #[planning_id] field

See Also

4 - List Variables

Ordered sequence variables for routing, sequencing, and scheduling problems.

List variables model problems where the solver must determine the order of elements in a sequence — not just which value is assigned, but what comes before and after. This is essential for vehicle routing (stop ordering), job shop scheduling (operation sequencing), and similar problems.

When to Use List Variables

Use list variables when:

  • The order of assignments matters (routes, sequences)
  • Each element belongs to exactly one list
  • The solver needs to optimize both assignment and ordering

Use basic planning variables when:

  • Only the assignment matters, not the order
  • Multiple entities can share the same value

The ListVariableSolution Trait

List variable solutions implement ListVariableSolution to define the relationship between lists and their elements.

use solverforge::prelude::*;

#[problem_fact]
#[derive(Clone, Debug)]
pub struct Stop {
    #[planning_id]
    pub id: i64,
    pub location: Location,
    pub demand: i32,
}

#[planning_entity]
#[derive(Clone, Debug)]
pub struct Vehicle {
    #[planning_id]
    pub id: i64,
    pub capacity: i32,
    pub depot: Location,
    #[planning_list_variable]
    pub stops: Vec<Stop>,
}

#[planning_solution(constraints = "crate::constraints::define_constraints")]
pub struct VehicleRoutePlan {
    #[problem_fact_collection]
    pub stops: Vec<Stop>,
    #[planning_entity_collection]
    pub vehicles: Vec<Vehicle>,
    #[planning_score]
    pub score: Option<HardSoftScore>,
}

Shadow Variables for Lists

List variables support shadow variables that automatically track predecessor and successor relationships:

#[problem_fact]
#[derive(Clone, Debug)]
pub struct Stop {
    #[planning_id]
    pub id: i64,

    #[previous_element_shadow_variable(source_variable = "stops")]
    pub previous_stop: Option<Stop>,

    #[next_element_shadow_variable(source_variable = "stops")]
    pub next_stop: Option<Stop>,

    #[inverse_relation_shadow_variable(source_variable = "stops")]
    pub vehicle: Option<Vehicle>,
}

These shadow variables are maintained automatically as the solver moves elements between lists and reorders them.

List Moves

The solver uses specialized moves for list variables:

MoveDescription
ListChangeMoveMove an element from one list to another (or within the same list)
ListSwapMoveSwap two elements between or within lists
ListReverseMoveReverse a subsequence within a list
SubListChangeMoveMove a contiguous subsequence to another position
SubListSwapMoveSwap two contiguous subsequences
KOptMoveK-opt style moves for routing problems
RuinMoveRemove elements and reinsert them (ruin-and-recreate)

Example: Vehicle Routing

fn define_constraints() -> impl ConstraintSet<VehicleRoutePlan, HardSoftScore> {
    let factory = ConstraintFactory::<VehicleRoutePlan, HardSoftScore>::new();

    (
        // Hard: don't exceed vehicle capacity
        factory.for_each(|s: &VehicleRoutePlan| s.vehicles.as_slice())
            .filter(|v| v.total_demand() > v.capacity)
            .penalize_hard_with(|v: &Vehicle| {
                HardSoftScore::of_hard((v.total_demand() - v.capacity) as i64)
            })
            .named("Capacity"),

        // Soft: minimize total driving distance
        factory.for_each(|s: &VehicleRoutePlan| s.vehicles.as_slice())
            .penalize_with(|v: &Vehicle| HardSoftScore::of_soft(v.total_distance()))
            .named("Distance"),
    )
}

See Also