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
- Select entities or facts from your solution using generated source methods
- Filter, project, join, or group to narrow down the matches
- Penalize or reward to affect the score
- 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. Tri, Quad, and Penta provide named
arity adapters; beyond five sources, Chain continues at arbitrary depth.
In v0.19.9 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, and filtered keyed joins
retain the filter contract on both sides of the join.
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
- Constraint Streams - The core stream API
- Constraint Node Sharing -
#[solverforge_constraints]and repeated grouped terminal sharing - Constraint Factory Methods -
ConstraintFactory, generated collection sources, and lower-levelfor_each - Projected Scoring Rows - Scoring-only rows, joined-pair
.project(...), and directed projected self-joins - Existence & Flattening -
if_exists,if_not_exists, andflatten_last - Joiners - Combining multiple entity streams
- Collectors - Aggregation functions for
group_by - Score Types - Available score types and when to use each
- Score Analysis - Understanding and explaining scores