Documentation
Constraint Streams
Declarative constraint definition using the stream API.
Constraint streams are the primary way to define constraints in SolverForge. They provide a pipeline-style API where you select entities or facts, transform the stream, and terminate with a scoring impact.
Defining Constraints
Constraints are defined as a function that returns a tuple of constraint
objects. The #[planning_solution] macro wires this up automatically.
use solverforge::prelude::*;
use solverforge::stream::{joiner::*, ConstraintFactory};
fn define_constraints() -> impl ConstraintSet<Schedule, HardSoftScore> {
let factory = ConstraintFactory::<Schedule, HardSoftScore>::new();
(
factory.shifts()
.filter(|shift| shift.employee_idx.is_none())
.penalize(HardSoftScore::ONE_HARD)
.named("Unassigned shift"),
)
}
Each constraint builder chain produces an IncrementalUniConstraint,
IncrementalBiConstraint, or related constraint object through .named().
Return them as a tuple; SolverForge implements ConstraintSet for tuples of up
to 16 constraints.
Source Operations
Generated Accessors
Generated {Name}ConstraintStreams accessors select all items from a solution
collection and carry hidden source metadata for localized incremental scoring.
type Streams = ConstraintFactory<Schedule, HardSoftScore>;
Streams::new().shifts()
Streams::new().employees()
These should be the default entry points for planning entity and problem fact collections. See Constraint Factory Methods for the generated method contract.
for_each
for_each selects items from a solution collection using a closure extractor.
Use generated accessors when they exist; use for_each for lower-level or
custom collection surfaces.
use solverforge::stream::vec;
factory.for_each(vec(|solution: &Schedule| &solution.custom_rows))
Intermediate Operations
| Operation | Purpose |
|---|---|
filter |
Keep only matches that satisfy a predicate |
join |
Combine rows from the same stream or a second stream |
project |
Create retained scoring-only rows |
flatten_last |
Expand a collection carried by the last joined item |
group_by |
Group rows and apply a collector |
balance |
Score load balance without manual grouped unfairness logic |
if_exists / if_not_exists |
Keep rows based on matching rows in another collection |
filter
factory.shifts()
.filter(|shift| shift.employee_idx.is_none())
join
join dispatches on the target shape.
Self-join with an equal joiner:
factory.shifts()
.join(equal(|shift: &Shift| shift.employee_idx))
Cross-join with a generated accessor plus equal_bi:
type Streams = ConstraintFactory<Schedule, HardSoftScore>;
Streams::new()
.shifts()
.join((
Streams::new().unavailability(),
equal_bi(
|shift: &Shift| shift.employee_idx,
|u: &Unavailability| u.employee_idx,
),
))
See Joiners for joiner types and composition.
group_by
factory.shifts()
.group_by(
|shift: &Shift| shift.employee_idx,
count(),
)
See Collectors for count, sum,
and load_balance.
balance
balance calculates load imbalance across a grouping key. The key function
returns Option<K>; None values are skipped, which is useful for unassigned
entities.
factory.shifts()
.balance(|shift: &Shift| shift.employee_idx)
Terminal Operations
penalize / reward
Apply a fixed score impact per match, then finalize with .named().
type Streams = ConstraintFactory<Schedule, HardSoftScore>;
let hard = Streams::new()
.shifts()
.penalize(HardSoftScore::ONE_HARD)
.named("Constraint name");
let soft = Streams::new()
.shifts()
.reward(HardSoftScore::ONE_SOFT)
.named("Preference bonus");
Score Convenience Methods
Use convenience methods when the constraint applies one hard or soft unit:
type Streams = ConstraintFactory<Schedule, HardSoftScore>;
let hard = Streams::new()
.shifts()
.penalize_hard()
.named("Hard violation");
let soft = Streams::new()
.shifts()
.reward_soft()
.named("Soft preference");
Use dynamic methods when the score depends on the match:
type Streams = ConstraintFactory<Schedule, HardSoftScore>;
let overtime = Streams::new()
.shifts()
.penalize_hard_with(|shift: &Shift| HardSoftScore::of_hard(shift.overtime_hours() as i64))
.named("Overtime");
let preference = Streams::new()
.shifts()
.penalize_with(|shift: &Shift| HardSoftScore::of_soft(shift.preference_penalty()))
.named("Preference");
Full Example
use solverforge::prelude::*;
use solverforge::stream::{joiner::*, ConstraintFactory};
fn define_constraints() -> impl ConstraintSet<Schedule, HardSoftScore> {
type Streams = ConstraintFactory<Schedule, HardSoftScore>;
(
Streams::new()
.shifts()
.filter(|shift| shift.employee_idx.is_none())
.penalize_hard()
.named("Unassigned shift"),
Streams::new()
.shifts()
.join(equal(|shift: &Shift| shift.employee_idx))
.filter(|a: &Shift, b: &Shift| {
a.employee_idx.is_some() && a.overlaps(b)
})
.penalize_hard()
.named("Overlap"),
Streams::new()
.shifts()
.filter(|shift| shift.is_preferred_by_employee())
.reward_soft()
.named("Preference"),
)
}
See Also
- Projected Scoring Rows - scoring-only derived rows
- Constraint Factory Methods - generated collection accessors and
for_each - Existence & Flattening -
if_exists,if_not_exists, andflatten_last - Score Analysis - inspecting score contributions