Relations and scores
Hard relations tie two activities together in time. Score rules express preferences on three levels, and every solution reports how each rule contributed.
Activity relations
An ActivityRelationConstraint connects two activities with an ActivityRelation. It is a hard
constraint: the solver never returns a solution that violates it. With .soft(SoftPenalty::new(category, level, weight)) it becomes a preference instead, and a broken relation costs weight.
| Relation | Meaning |
|---|---|
SameStart |
Both activities start at the same time. |
Consecutive |
The second starts exactly when the first ends. |
Precedence { min_gap } |
The first ends at least min_gap units before the second starts. |
NoOverlap |
The two activities do not overlap in time. |
FixedOffset { offset } |
The second starts exactly offset units after the first starts (negative: before). |
use schedulr::{ActivityId, ActivityRelation, ActivityRelationConstraint};
let problem = problem
.with_relation(ActivityRelationConstraint::new(
ActivityId(2),
ActivityId(3),
ActivityRelation::Consecutive,
))
.with_relation(ActivityRelationConstraint::new(
ActivityId(1),
ActivityId(2),
ActivityRelation::Precedence { min_gap: 1 },
));
NoOverlap is useful for activities that share no resource or participant, for example a
welcome session and an office hour that everyone should be able to attend. Activities that share
a resource with capacity 1 or a participant never overlap anyway.
Relations must reference known activities and cannot relate an activity to itself; both are compile errors.
Score rules
Preferences are ScoreRules. Each rule has a category (a name you choose), a level, the activity
it applies to, a positive weight, and a kind. ScoreRule::prefer_window,
ScoreRule::prefer_within_bucket and ScoreRule::avoid_within_bucket construct them:
use schedulr::{ActivityId, ScoreLevel, ScoreRule, TimeWindow};
let problem = problem.with_score_rule(ScoreRule::prefer_window(
"kickoff on Monday at 09:00",
ScoreLevel::Strong,
ActivityId(1),
TimeWindow::new(9, 10),
1,
));
A PreferWindow rule costs -weight when the activity starts outside the preferred window and
0 when it starts inside. PreferWithinBucket and AvoidWithinBucket name a stretch of every
period bucket instead, by offset from the bucket’s start — “not in the first period of a day”.
KeepStart is used by repair to keep activities where they were.
Preferences that concern a person, a resource or a whole course rather than one activity are
goals; the soft form of a day rule is described in
Day and bucket rules. All of them are scored on the same three
levels and reported in the same score_components.
Score levels
ScoreLevel has three tiers: Strong, Medium and Weak. They are compared
lexicographically: any improvement on a higher level beats every possible improvement on a lower
one, no matter the weights. Weights only compare rules on the same level.
When a problem has score rules or goals, solve() constructs a feasible plan and then optimises the
score; without rules it returns the first feasible plan it constructs (see
Solving).
SolveStatistics::optimal tells you whether the search proved the result optimal.
Reading the score
Every Solution carries:
score: aScorewith the fieldshard,strong,medium,weakandsoft;score_components: oneScoreComponentper rule with its category, level, activity and value.
The components let an application show why a plan looks the way it does, and store the scores with each schedule version. From the workshop plan:
Score: hard 0, strong 0, medium 0, weak 0
Strong kickoff on Monday at 09:00 a1 0
Medium Q&A on Tuesday afternoon a5 0
Weak lab on Tuesday morning a3 0
When several plans reach the same best score, the one solve() returns depends on how the
default portfolio’s workers happen to be scheduled and can differ between runs. Add rules if you
need to prefer one of them, or solve with SolveStrategy::ConstructThenOptimize, which repeats.