schedulrv0.14.0

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 ActivityDomain conflict for each such activity — never a panic;
  • a modeled activity without an assignment is a blocking Unassigned conflict, 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_feasible and the blocking hard_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 Strong rule that penalises a start different from its baseline start by change_penalty;
  • every resource/participant selection that the solver is free to choose gets a Strong rule that penalises a value different from the baseline by assignment_change_penalty, so a repair cannot silently swap people or rooms elsewhere in the plan;
  • activities listed in frozen are 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_limit or the cancellation token 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.

Edit this page on GitHub · Docs for v0.14.0