Planning Variables
A planning variable is a property of a planning entity that the solver assigns values to during optimization.
Simple Planning Variable
The most common type assigns a single value from a value range:
from dataclasses import dataclass, field
from typing import Annotated
from solverforge_legacy.solver.domain import planning_entity, PlanningId, PlanningVariable
@planning_entity
@dataclass
class Lesson:
id: Annotated[str, PlanningId]
subject: str
# Simple planning variable
timeslot: Annotated[Timeslot | None, PlanningVariable] = field(default=None)
room: Annotated[Room | None, PlanningVariable] = field(default=None)
How It Works
- The solver sees
timeslotneeds a value - It looks for a
ValueRangeProviderforTimeslotin the solution - It tries different values and evaluates the score
- It assigns the best value found within the time limit
Planning List Variable
For routing problems where order matters, use PlanningListVariable:
from solverforge_legacy.solver.domain import PlanningListVariable
@planning_entity
@dataclass
class Vehicle:
id: Annotated[str, PlanningId]
capacity: int
home_location: Location
# List variable - ordered sequence of visits
visits: Annotated[list[Visit], PlanningListVariable] = field(default_factory=list)
How It Works
The solver:
- Assigns visits to vehicles
- Determines the order of visits within each vehicle’s route
- Uses moves like insert, swap, and 2-opt for optimization
When to Use List Variables
Use PlanningListVariable when:
- Order matters (routing, sequencing)
- Entities belong to groups (visits per vehicle, tasks per worker)
- Chain relationships exist (predecessor/successor patterns)
Nullable Variables
By default, all planning variables must be assigned. For optional assignments:
@planning_entity
@dataclass
class Visit:
id: Annotated[str, PlanningId]
location: Location
# This visit might not be assigned to any vehicle
vehicle: Annotated[Vehicle | None, PlanningVariable(allows_unassigned=True)] = field(default=None)
Note: When using nullable variables, add medium constraints to penalize unassigned entities.
Value Range Providers
Planning variables need a source of possible values. This is configured in the planning solution:
@planning_solution
@dataclass
class Timetable:
# This list provides values for 'timeslot' variables
timeslots: Annotated[list[Timeslot], ProblemFactCollectionProperty, ValueRangeProvider]
# This list provides values for 'room' variables
rooms: Annotated[list[Room], ProblemFactCollectionProperty, ValueRangeProvider]
lessons: Annotated[list[Lesson], PlanningEntityCollectionProperty]
score: Annotated[HardSoftScore, PlanningScore] = field(default=None)
The solver matches variables to value ranges by type:
timeslot: Annotated[Timeslot | None, PlanningVariable]useslist[Timeslot]room: Annotated[Room | None, PlanningVariable]useslist[Room]
Variable Configuration Options
Strength Comparator
For construction heuristics, you can specify how to order values:
# Stronger values tried first during construction
timeslot: Annotated[
Timeslot | None,
PlanningVariable(value_range_provider_refs=["timeslots"])
] = field(default=None)
Multiple Variables on One Entity
Entities can have multiple independent variables:
@planning_entity
@dataclass
class Lesson:
id: Annotated[str, PlanningId]
# Two independent variables
timeslot: Annotated[Timeslot | None, PlanningVariable] = field(default=None)
room: Annotated[Room | None, PlanningVariable] = field(default=None)
Each variable is optimized independently—assigning timeslot doesn’t affect room.
Chained Variables (Alternative to List)
For simpler routing without list variables, you can use chained planning variables. However, PlanningListVariable is generally easier and more efficient.
Variable Listener Pattern
When one variable affects another, use shadow variables:
@planning_entity
@dataclass
class Visit:
id: Annotated[str, PlanningId]
location: Location
# Calculated from vehicle's visit list
vehicle: Annotated[Vehicle | None, InverseRelationShadowVariable(source_variable_name="visits")] = field(default=None)
# Calculated from previous visit
arrival_time: Annotated[datetime | None, CascadingUpdateShadowVariable(target_method_name="update_arrival_time")] = field(default=None)
See Shadow Variables for details.
Best Practices
Do
- Initialize variables to
Noneor empty list - Use type hints with
| Nonefor nullable types - Match value range types exactly
Don’t
- Mix list variables with simple variables for the same concept
- Use complex types as planning variables (use references instead)
- Forget to provide a value range
Common Patterns
Scheduling
timeslot: Annotated[Timeslot | None, PlanningVariable] = field(default=None)
Assignment
employee: Annotated[Employee | None, PlanningVariable] = field(default=None)
Routing
visits: Annotated[list[Visit], PlanningListVariable] = field(default_factory=list)
Next Steps
- Planning Solutions - Define value ranges
- Shadow Variables - Calculated variables
Feedback
Was this page helpful?
Glad to hear it! Please tell us how we can improve.
Sorry to hear that. Please tell us how we can improve.