Documentation

Constraints

Define business rules using constraint streams, joiners, collectors, and score types.

Constraints are the business rules that define what makes a good solution. SolverForge uses a constraint streams API - a declarative, composable way to express rules that reads like a pipeline of filters and transformations.

How Constraints Work

  1. Select entities or facts from your solution using generated source methods
  2. Filter, project, join, or group to narrow down the matches
  3. Penalize or reward to affect the score
  4. Name the constraint with .named()
let factory = ConstraintFactory::<Schedule, HardSoftScore>::new();

factory.for_each(Schedule::shifts())                              // Select all shifts
    .filter(|s| s.employee_idx.is_none())     // Keep unassigned ones
    .penalize(HardSoftScore::ONE_HARD)        // Penalize each
    .named("Unassigned shift")                // Finalize

Constraints are returned as a tuple implementing ConstraintSet<S, Sc>, which the solver evaluates incrementally as it explores moves. Generated solution sources such as Schedule::shifts() preserve source metadata for localized incremental updates. Projected streams can emit retained scoring rows from one source or one joined pair, grouped streams can use collectors such as consecutive_runs(...), collect_vec(...), and indexed_presence(...), and projected self-joins can be symmetric with equal(...) or directed with equal_bi(left_key, right_key). Direct cross joins can group joined pairs without projecting them first. Chained joins use .join((target, equal_on(left_row_key, right_leaf_key))), with an independent key domain for each relationship. Every second or later join takes the whole borrowed left row and a right Leaf view; read fields through .entity. Plain entity-closure equal_bi is for the first join, not a continuation. Tri, Quad, and Penta provide named cross-stream arity adapters; cross Penta::join continues into Chain at arbitrary depth. That continuation drops an authored penta filter; apply the row filter on the returned Chain. Same-source PentaConstraintStream is terminal. Direct pair scoring, chained joins, existence, flattening, and unary complemented groups execute through concrete stream::relational::operator trees and the shared OperatorTerminal, not per-shape retained engines. Direct cross-join groups can also call complement(...) against generated target sources. On the first unary keyed equality cross-join, unary source membership is checked before keys and pairs are created; post-join filters remain row filters. This left-source pushdown does not apply to comparison, overlap, composed-condition, predicate, or same-source self-join dispatch. Fluent right targets implement CollectionExtract; arbitrary derived inputs belong at low-level JoinNode. See Constraint Streams for nullable-row examples and the exact dispatch boundary.

Annotate reusable constraint functions with #[solverforge_constraints] when the same grouped stream feeds multiple named terminal constraints. SolverForge then shares the retained grouped node work while keeping each terminal constraint’s name, ordering, metadata, and explanation independent. The lower-level constraint metadata borrows full ConstraintRef identity from the owning constraint. Package-qualified constraints use ConstraintRef::full_name() as the configured key; package-less constraints use the short name.

Sections