unifierv0.6.0

Changelog

Generated from CHANGELOG.md in the project repository. Never maintained twice.

Unreleased

  • changedWebsite: the start page shows a recorded run of the job_shop example 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 pathwise 0.3 instead of 0.2. CancellationToken is re-exported from pathwise, so code that names the 0.2 type needs pathwise 0.3 as well.
  • addedConstraintPenalty: an objective that turns any Constraint into a preference. Each violation the constraint counts (Constraint::violations) costs weight soft points, so a graded constraint such as MaximumBucketLoad or BucketBlockPattern hands the search its own gradient. Its scope is the constraint's, which keeps incremental scoring local.
  • addedObjective::delta and Constraint::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; CategorizedObjective forwards delta, NoOverlap prepares 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), as calculate_score does — one more clash inside an already broken constraint now costs something.
  • changedLocal search picks the conflicted variable from Constraint::explain where available, and collects a move's constraints, objectives and old share once per move instead of per value.

0.5.3

24 September 2026

  • changedBacktrackingSolver now 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 the dom/wdeg weight increment a propagation conflict gets. Before, a constraint whose propagate reported nothing was checked at the leaf alone: a violation decided near the root cost a full sweep of every variable below it. BranchAndBoundSolver never had that blind spot; its bound asks every constraint through optimistic_score. This brings the tree search level with it.

0.5.1

20 September 2026

  • fixedExactlyOne and AtLeast no longer report a violation under a partial assignment merely because nothing has reached the target yet. Constraint::is_satisfied documents that it answers false only when what is already assigned breaks the constraint — AllDifferent looks only at assigned variables, Equal compares only when both sides are there, AtMost counts upwards, and BucketLoad and MinimumDistance each pin the rule with a test of their own. These two counted == 1 and >= k outright, 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: SolveOutcome has a new best_effort field, 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 NoOverlap over 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 still hard == 0 and 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::offer records 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.
  • changedBacktrackingSolver and BranchAndBoundSolver descend 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.
  • changedLargeNeighborhoodSearch repairs 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 LocalSearchSolver takes 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.
  • addedConstraint::violations, answering how many ways a constraint is broken under an assignment — 0 exactly when is_satisfied is true. It has a default returning 1 per violation, so external implementations keep compiling and keep their old behaviour. AllDifferent, NoOverlap, Cumulative, MaximumBucketLoad and BucketBlockPattern override it; the hard score is the sum. Differential tests walk every assignment of a small instance per constraint to hold the 0 ⇔ satisfied equivalence, which feasibility now depends on.
  • addedSolveOutcome::best_effort and SolveOutcome::reached(). best_effort is the best complete assignment a run reached when it could not vouch for one, present on an Infeasible outcome too; it is set only while solution is None, 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.
  • addedSharedIncumbent::center(), the best complete assignment offered so far whether feasible or not. Read center() to decide where to work and best() to decide what to report.
  • addedLocalSearchSolver::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: SolverOptions has a new seed field, so a struct literal listing every field no longer compiles. ..SolverOptions::default() is unaffected.
  • changedBreaking: ParallelSolver adopts an incumbent the caller set in SolverOptions::shared_incumbent instead of discarding it and starting empty. A caller that already holds a solution can hand it over, and improvements flow back into that handle.
  • changedBranchAndBoundSolver orders variables by dom/wdeg instead of plain MRV, threading through its recursion the same constraint weights BacktrackingSolver already used, and tries values least-constraining first. Both solvers share that value ordering.
  • changedTies in the variable ordering resolve to the lowest VariableId rather than to HashMap iteration 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.
  • changedLocalSearchSolver repairs 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.
  • changedBacktrackingSolver restarts on a Luby schedule, carrying its dom/wdeg weights 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 reports Infeasible.
  • 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.
  • addedPropagationEngine::propagate_from, taking the seed constraints. propagate is now the special case that seeds everything, which is still what a root-level call wants.
  • addedSolverOptions::seed for the randomized tie-breaking in LocalSearchSolver, fixed by default so a run replays exactly.
  • removedselect_mrv_variable (crate-internal), which lost its last caller.

0.3.2

14 September 2026

  • addedMinimumDistance constraint and ModelBuilder::add_minimum_distance: two variables must be at least a given distance apart. Propagation prunes the other side once one side is fixed.
  • addedMaximumBucketLoad constraint and ModelBuilder::add_maximum_bucket_load: caps the summed demand × occupied time of a group of BucketedTasks inside every bucket, described by half-open BucketRanges (for example one per day). Optional tasks count only while their presence variable is 1.
  • addedBucketBlockPattern constraint and ModelBuilder::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].
  • addedBucketRange, BucketedTask, MaximumBucketLoad and MinimumDistance are re-exported at the crate root.

0.3.1

4 September 2026

  • fixedLnsSolver no 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.
  • fixedHardSoftScore's ordering also compares the summed soft score last, so it agrees with equality.

0.3.0

4 September 2026

  • addedLexicographic soft-score levels: HardSoftScore has strong, medium and weak next to the summed soft, HardSoftScore::tiered builds such a score, and ScoreLevel names the levels.
  • addedObjective::category and Objective::level (defaults: the objective's name and ScoreLevel::Weak), CategorizedObjective to set both for any objective, and ModelBuilder::add_scored_objective.
  • addedPeriodicValues constraint and ModelBuilder::add_periodic_calendar: allowed offsets in a repeating period plus absolute unavailable ranges, stored independently of the horizon.
  • addedLnsSolver::solve_from: repair from a caller-provided baseline assignment, even one that changed constraints have made infeasible.
  • changedScoreCalculator totals each soft level separately.
  • changedLnsSolver only keeps feasible solutions as its best solution and shared incumbent.
  • breakingHardSoftScore has new public fields, so struct literals no longer compile; use HardSoftScore::new or HardSoftScore::tiered.
  • breakingHardSoftScore prints every level, for example Feasible(strong=0, medium=0, weak=-11) instead of Feasible(-11).
  • breakingModelBuilder::add_constraint returns the new ConstraintId.

0.2.0

4 September 2026

  • addedValidatedGraph::check_incremental: evaluates only the constraints touched by changed variables, without starting a search, and returns ConstraintViolations.
  • addedStructured constraint explanations (Constraint::explain, Explanation), with specialized messages for NoOverlap, Cumulative and Precedence.
  • changedRequires pathwise 0.2.0 instead of 0.1.0.
  • breakingModelBuilder::compile_scheduling_model returns the ResourceId to ConstraintId mapping it built (Result<HashMap<ResourceId, ConstraintId>, _> instead of Result<(), _>), 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.