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. 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
- 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 - Shift-Group Uniformity - A worked grouped-penalty example over operators
- Score Types - Available score types and when to use each
- Score Analysis - Understanding and explaining scores