Analysis and repair
Control a solve run, explain why a problem has no plan, estimate pressure before solving, evaluate manual edits, compare two plans, and repair a plan with as few changes as possible.
All of these work on a CompiledProblem, the result of compile.
Solving
solve() is solve_with(&SolveOptions::default()): a time limit of ten seconds, no
cancellation token and the default SolveStrategy. Every strategy first constructs a schedule
that satisfies the hard constraints; without score rules or goals that is the whole run.
SolveStrategy |
After construction |
|---|---|
ConstructThenPortfolio (default) |
unifier’s four-worker portfolio (backtracking, local search, LNS, branch and bound) improves the schedule for the remaining budget |
ConstructThenOptimize |
branch and bound improves it; repeatable from run to run |
OptimizeOnly |
no construction phase, one branch-and-bound run over the whole budget |
SolveOptions has three public fields: time_limit (None searches without a limit),
cancellation_token and strategy. A CancellationToken stops a running search from another
thread; clones share one flag, so keep a clone and call cancel() on it:
use schedulr::{CancellationToken, SolveOptions, SolveStrategy};
use std::time::Duration;
let token = CancellationToken::new();
let options = SolveOptions {
time_limit: Some(Duration::from_secs(30)),
cancellation_token: Some(token.clone()),
strategy: SolveStrategy::ConstructThenOptimize,
};
// Elsewhere, for example when the user closes the dialog: token.cancel();
let result = compiled.solve_with(&options);
A cancelled search returns SolveStatus::Aborted(AbortReason::Cancelled).
A search that hits the limit returns SolveStatus::Aborted with the best feasible solution found
so far, if any. A schedule that violates hard constraints is never returned as solution; the
assignment such a run reached is kept in best_effort, and check explains it. statistics
reports the expanded nodes, the elapsed time and whether the result is proven optimal.
explain
explain(&result) returns an empty list for a result with a solution. For an infeasible result
it evaluates the constraints on every activity whose start is already fixed by its window and
reports the violated ones. For flexible requirements it also checks whether the candidate
resources could hold all activities even if every alternative were used, and reports
AlternativeResourceCapacity when they cannot.
Blocking AlternativeResourceCapacity [a1, a2, a3]: Room Aurora: capacity 1 is insufficient for the unresolved alternatives
explain finds conflicts that are visible from fixed windows and capacities. It does not compute
a minimal set of conflicting constraints for every infeasible problem.
check
check(&solution) is the whole-solution counterpart of evaluate_move: one call evaluates every
constraint against a complete or partial Solution without changing it. The SolutionCheck has
the same verdict fields as a change evaluation — is_feasible, hard_violations, warnings,
explanations, plus the absolute score and score_components of the checked plan. It is what a
stored version should be validated with; it replaces one no-op move per assignment.
It is stricter than evaluate_move about completeness, on purpose:
- an assignment naming an activity the problem does not contain is a blocking
ActivityDomainconflict for each such activity — never a panic; - a modeled activity without an assignment is a blocking
Unassignedconflict, so a partial solution is explicit instead of silently accepted.
let check = compiled.check(&stored_version);
if !check.is_feasible {
for conflict in &check.hard_violations {
eprintln!("{}", conflict.message);
}
}
analyze
analyze() estimates pressure without solving. For each resource and participant it compares the
required time (duration times units, spread evenly over the candidate resources of a flexible
requirement) with the available time over the horizon (the academic period, or the span of all
activity windows). The result is a list of Bottlenecks sorted by utilisation, and a
feasibility_percent of 100 divided by the highest utilisation, capped at 100. A value below 100
means at least one resource or person is overbooked on paper.
evaluate_move and suggest
evaluate_move(&solution, activity, window) checks moving one activity of a solution to a new
window. The window must keep the activity’s duration and stay inside its allowed window. The
MoveEvaluation contains:
is_feasibleand the blockinghard_violations,- advisory
warnings, score_delta, the score change the move would cause,score_components, the score per rule after the move,explanations, the conflict messages as text.
suggest(&solution, window) runs evaluate_move for every activity and returns the feasible
ones, best score delta first: “which activity could go into this free slot?”.
evaluate_changes
evaluate_changes(&solution, &changes) evaluates several edits at once, as one atomic batch. A
ChangeRequest is one of:
ChangeRequest |
Edit |
|---|---|
Move { activity, window } |
Moves the activity; the duration stays and the window must lie inside the activity’s allowed window |
Swap { first, second } |
Exchanges the windows of two activities of equal duration |
ReplaceResource { activity, from, to } |
Uses resource to instead of from; to must be a candidate of the requirement from fills |
ReplaceParticipant { activity, from, to } |
Hands the activity from from to to; to must be a candidate of the same participant requirement |
Every request refers to the windows of the starting solution, which is never changed. A batch is
all or nothing: the first invalid request, or a starting solution naming an activity the problem
no longer contains, makes the whole batch a blocking ActivityDomain violation.
use schedulr::{ActivityId, ChangeRequest, ParticipantId};
let evaluation = compiled.evaluate_changes(&solution, &[
ChangeRequest::Swap { first: ActivityId(1), second: ActivityId(4) },
ChangeRequest::ReplaceParticipant {
activity: ActivityId(4),
from: ParticipantId(1),
to: ParticipantId(3),
},
]);
if evaluation.is_feasible {
store(evaluation.candidate);
}
The ChangeEvaluation has the same fields as a MoveEvaluation (is_feasible,
hard_violations, warnings, score_delta, score_components, explanations) plus
candidate, the plan the batch would produce. candidate is built for refused batches too, so a
UI can show what was attempted; check is_feasible before storing it.
compare
compare(&before, &after) is a free function. It lists every AssignmentChange (Added,
Removed or Changed, with the assignment before and after) and the score_delta between two
solutions, for example to show a reviewer what a re-plan changed. AssignmentChange::aspects()
returns ChangeAspects { time, resources, participants }, three flags saying which parts of the
assignment differ; Added and Removed set all three. What a changed participant means (a new
teacher, a different class) is left to your application.
repair
repair(&baseline, &options) re-solves after the problem changed (a room became smaller, an
activity was added) and tries to keep the baseline:
- every activity of the baseline gets a
Strongrule that penalises a start different from its baseline start bychange_penalty; - every resource/participant selection that the solver is free to choose gets a
Strongrule that penalises a value different from the baseline byassignment_change_penalty, so a repair cannot silently swap people or rooms elsewhere in the plan; - activities listed in
frozenare pinned entirely (time, participants, resources) to their baseline values; - a large neighbourhood search (LNS) starts from the baseline, repeatedly frees a fraction of the
activities and re-solves them, until
time_limitor thecancellationtoken stops it.
use schedulr::{ActivityId, RepairOptions};
use std::time::Duration;
let outcome = compiled.repair_with_outcome(&baseline, &RepairOptions {
time_limit: Duration::from_millis(100),
frozen: [ActivityId(1)].into_iter().collect(),
..RepairOptions::default()
});
if outcome.outcome == schedulr::RepairOutcome::FellBackToSolve {
// Compilation failed; the result is a fresh solve, not a repair of the baseline.
}
let repaired = outcome.result;
The defaults are change_penalty: 1_000_000, assignment_change_penalty: None (which means: use
change_penalty, so both stay aligned unless you set the assignment penalty explicitly), no frozen
activities, no cancellation token, destroy_fraction: 0.3 and a time limit of one second. Because
the stability rules sit on the Strong level, they outweigh all Medium and Weak preferences;
your own Strong rules compete with them through their weights.
repair returns the same SolveResult as repair_with_outcome(...).result. If the
stability-augmented problem does not compile (for example a baseline naming an activity the
problem no longer contains), repair_with_outcome reports RepairOutcome::FellBackToSolve: the
plain repair would otherwise present a fresh plan as if it were a repair.