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:
| Concept | Macro | Purpose |
|---|
| 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
- Annotate your structs with the appropriate derive macros
- Mark fields with attribute macros to tell the solver their role
- 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 Fact | Planning Entity |
|---|
| Modified by solver? | No | Yes |
| Has planning variables? | No | Yes |
| Example | Employee, Room, Timeslot | Shift, Lesson, Visit |
| Role | Input data / possible values | Things 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:
| Move | Description |
|---|
ListChangeMove | Move an element from one list to another (or within the same list) |
ListSwapMove | Swap two elements between or within lists |
ListReverseMove | Reverse a subsequence within a list |
SubListChangeMove | Move a contiguous subsequence to another position |
SubListSwapMove | Swap two contiguous subsequences |
KOptMove | K-opt style moves for routing problems |
RuinMove | Remove 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