Changelog
Generated from CHANGELOG.md in the project repository. Never maintained twice.
Unreleased
- changedWebsite: the start page shows a recorded run of the
job_shopexample and a chart of its optimal schedule; the full quickstart program lives in the documentation. - changedWebsite: the footer now follows the page language instead of always showing German text, and the start page carries a release teaser and an FAQ (theme 0.11.1).
0.6.0
24 October 2026
- breakingunifier builds on
pathwise0.3 instead of 0.2.CancellationTokenis re-exported frompathwise, so code that names the 0.2 type needspathwise0.3 as well. - added
ConstraintPenalty: an objective that turns anyConstraintinto a preference. Each violation the constraint counts (Constraint::violations) costsweightsoft points, so a graded constraint such asMaximumBucketLoadorBucketBlockPatternhands the search its own gradient. Its scope is the constraint's, which keeps incremental scoring local. - added
Objective::deltaandConstraint::move_violations: an objective can answer a move with the change it causes, and a constraint can prepare an evaluator once per move that answers each candidate value. Defaults keep the full evaluation;CategorizedObjectiveforwardsdelta,NoOverlapprepares an evaluator (pairs among the other tasks counted once). Local search uses both — on timetable repairs 2.7–5× more steps per second. - changedRepair from an infeasible baseline builds chains of moves: while the LNS center breaks hard constraints, the conflict search runs on the whole model without a node cap instead of on a frozen neighbourhood (a blocked period in a full timetable went from 7 of 8 conflicts left to none).
- changedLocal search scores moves graded (
Constraint::violations), ascalculate_scoredoes — one more clash inside an already broken constraint now costs something. - changedLocal search picks the conflicted variable from
Constraint::explainwhere available, and collects a move's constraints, objectives and old share once per move instead of per value.
0.5.3
24 September 2026
- changed
BacktrackingSolvernow asks the constraints of each newly assigned variable whether they can still be satisfied (Constraint::is_satisfiable) after propagation succeeds, and treats a "no" as a conflict — including thedom/wdegweight increment a propagation conflict gets. Before, a constraint whosepropagatereported nothing was checked at the leaf alone: a violation decided near the root cost a full sweep of every variable below it.BranchAndBoundSolvernever had that blind spot; its bound asks every constraint throughoptimistic_score. This brings the tree search level with it.
0.5.1
20 September 2026
- fixed
ExactlyOneandAtLeastno longer report a violation under a partial assignment merely because nothing has reached the target yet.Constraint::is_satisfieddocuments that it answersfalseonly when what is already assigned breaks the constraint —AllDifferentlooks only at assigned variables,Equalcompares only when both sides are there,AtMostcounts upwards, andBucketLoadandMinimumDistanceeach pin the rule with a test of their own. These two counted== 1and>= koutright, so a group nobody had chosen in yet called itself broken while every variable in it could still become the one.
0.5.0
20 September 2026
- changedBreaking:
SolveOutcomehas a newbest_effortfield, so a struct literal listing every field no longer compiles. The constructors are unaffected. - changedBreaking: the hard score sums how *badly* each constraint is broken instead of counting the constraints that answered "not satisfied". A
NoOverlapover forty time points used to cost the same for one collision as for five, so a move that resolved a collision *inside* an already-broken constraint had score delta zero — invisible to tabu memory, to Branch & Bound as a bound, and to every acceptance rule.is_feasible()is stillhard == 0and means what it meant; only the numbers in between changed, and any caller comparing hard scores across runs of different versions will see different magnitudes. - changedBreaking:
SharedIncumbent::offerrecords what it is given instead of dropping anything infeasible.best()is unchanged — still feasible-only, still what a portfolio returns — but callers no longer need to pre-filter, and the near miss a repair search wants now survives. - changed
BacktrackingSolverandBranchAndBoundSolverdescend over an explicit stack on the heap rather than over the call stack. The depth of an assignment search is the number of variables, so a recursive descent overflowed on large instances — and a stack overflow aborts the process, leaving the caller with no result where it should have got "nothing within the budget". Two guards pin it: 1000 variables on a 64 KiB stack, cross-checked against the recursive version. Nothing gets faster from this; a large instance now reports its time limit instead of taking the process with it. - changed
LargeNeighborhoodSearchrepairs an infeasible center with Local Search instead of asking an exact sub-search for a fully feasible sub-assignment. A neighbourhood that contains a violated constraint only in part is unsolvable by construction, and the constraints in question span dozens of variables — measured, not one round in thirty seconds improved anything. While the center is feasible the exact sub-search stays, which is the right tool for that question. Also fixed: an exhausted LNS loop could spend a second full time limit. - changedThe greedy construction in
LocalSearchSolvertakes back what stands in the way instead of keeping a conflicted value: when no conflict-free value exists it un-assigns the *other* variables in the violated constraints and re-queues them, bounded per variable so the procedure stays finite. Tightest domains go first, ties shuffled, and several attempts run with the best kept. Measured separately — conflict-directed rather than most-recent undo, and tightest-first ordering, each account for a large part of it: on the reference instance the construction went from 22 violations to none. - added
Constraint::violations, answering how many ways a constraint is broken under an assignment —0exactly whenis_satisfiedistrue. It has a default returning1per violation, so external implementations keep compiling and keep their old behaviour.AllDifferent,NoOverlap,Cumulative,MaximumBucketLoadandBucketBlockPatternoverride it; the hard score is the sum. Differential tests walk every assignment of a small instance per constraint to hold the0⇔ satisfied equivalence, which feasibility now depends on. - added
SolveOutcome::best_effortandSolveOutcome::reached().best_effortis the best complete assignment a run reached when it could not vouch for one, present on anInfeasibleoutcome too; it is set only whilesolutionisNone, so the two can never be confused.reached()is the question a repair search asks: somewhere to continue from, regardless of how the previous run ended. - added
SharedIncumbent::center(), the best complete assignment offered so far whether feasible or not. Readcenter()to decide where to work andbest()to decide what to report. - added
LocalSearchSolver::repair_from, which repairs a caller's assignment instead of building its own. A contradiction in root propagation is not a reason to stop here — a fixed assignment being contradictory is what "repair" means — and the narrowed domains are adopted only when they came out consistent.
0.4.0
18 September 2026
- changedBreaking:
SolverOptionshas a newseedfield, so a struct literal listing every field no longer compiles...SolverOptions::default()is unaffected. - changedBreaking:
ParallelSolveradopts an incumbent the caller set inSolverOptions::shared_incumbentinstead of discarding it and starting empty. A caller that already holds a solution can hand it over, and improvements flow back into that handle. - changed
BranchAndBoundSolverorders variables bydom/wdeginstead of plain MRV, threading through its recursion the same constraint weightsBacktrackingSolveralready used, and tries values least-constraining first. Both solvers share that value ordering. - changedTies in the variable ordering resolve to the lowest
VariableIdrather than toHashMapiteration order, which Rust randomizes per process. Before this, five runs over one unchanged model split three ways solved and twice never finished, purely by hash seed — no run was reproducible, so no measurement of this solver meant anything. - changed
LocalSearchSolverrepairs conflicts instead of rescanning the model: while hard constraints are violated it draws a violated constraint and moves one of its variables, scores candidates in place rather than cloning the assignment per candidate, and starts from AC-3 plus a greedy pass instead of every variable's domain minimum. On graph colouring, throughput at 240 variables went from 5.6k to 315k moves per second and stopped falling as the model grows. - changed
BacktrackingSolverrestarts on a Luby schedule, carrying itsdom/wdegweights across restarts. A run cut short by a restart budget is tracked apart from one that exhausted the search space, so only the latter still reportsInfeasible. - changedA search node propagates from the constraints of the variable it just assigned rather than from every constraint in the graph. Node counts across the benchmark corpus are unchanged to the digit — the same tree, reached about six times faster on a large model.
- added
PropagationEngine::propagate_from, taking the seed constraints.propagateis now the special case that seeds everything, which is still what a root-level call wants. - added
SolverOptions::seedfor the randomized tie-breaking inLocalSearchSolver, fixed by default so a run replays exactly. - removed
select_mrv_variable(crate-internal), which lost its last caller.
0.3.2
14 September 2026
- added
MinimumDistanceconstraint andModelBuilder::add_minimum_distance: two variables must be at least a given distance apart. Propagation prunes the other side once one side is fixed. - added
MaximumBucketLoadconstraint andModelBuilder::add_maximum_bucket_load: caps the summeddemand × occupied timeof a group ofBucketedTasks inside every bucket, described by half-openBucketRanges (for example one per day). Optional tasks count only while their presence variable is1. - added
BucketBlockPatternconstraint andModelBuilder::add_bucket_block_pattern: the consecutive blocks a group of tasks occupies across all buckets must form one of the allowed block-length multisets, such as[2, 1, 1]. - added
BucketRange,BucketedTask,MaximumBucketLoadandMinimumDistanceare re-exported at the crate root.
0.3.1
4 September 2026
- fixed
LnsSolverno longer returns an assignment of domain minimums, which could violate constraints, when it has nothing to destroy or ends without a feasible solution; it falls back to the repair solver instead. - fixed
HardSoftScore's ordering also compares the summedsoftscore last, so it agrees with equality.
0.3.0
4 September 2026
- addedLexicographic soft-score levels:
HardSoftScorehasstrong,mediumandweaknext to the summedsoft,HardSoftScore::tieredbuilds such a score, andScoreLevelnames the levels. - added
Objective::categoryandObjective::level(defaults: the objective's name andScoreLevel::Weak),CategorizedObjectiveto set both for any objective, andModelBuilder::add_scored_objective. - added
PeriodicValuesconstraint andModelBuilder::add_periodic_calendar: allowed offsets in a repeating period plus absolute unavailable ranges, stored independently of the horizon. - added
LnsSolver::solve_from: repair from a caller-provided baseline assignment, even one that changed constraints have made infeasible. - changed
ScoreCalculatortotals each soft level separately. - changed
LnsSolveronly keeps feasible solutions as its best solution and shared incumbent. - breaking
HardSoftScorehas new public fields, so struct literals no longer compile; useHardSoftScore::neworHardSoftScore::tiered. - breaking
HardSoftScoreprints every level, for exampleFeasible(strong=0, medium=0, weak=-11)instead ofFeasible(-11). - breaking
ModelBuilder::add_constraintreturns the newConstraintId.
0.2.0
4 September 2026
- added
ValidatedGraph::check_incremental: evaluates only the constraints touched by changed variables, without starting a search, and returnsConstraintViolations. - addedStructured constraint explanations (
Constraint::explain,Explanation), with specialized messages forNoOverlap,CumulativeandPrecedence. - changedRequires
pathwise0.2.0 instead of 0.1.0. - breaking
ModelBuilder::compile_scheduling_modelreturns theResourceIdtoConstraintIdmapping it built (Result<HashMap<ResourceId, ConstraintId>, _>instead ofResult<(), _>), so a violation can be attributed to its resource.
0.1.1
1 September 2026
- addedCriterion benchmarks for the solvers.
- fixedStale references in the README.
0.1.0
1 September 2026
- addedFirst release: constraint graph with validation, built-in and global constraints, hard/soft scoring, five solvers (Backtracking, Branch & Bound, Local Search, LNS, parallel portfolio) and scheduling primitives.