UniConstraintStream

Struct UniConstraintStream 

Source
pub struct UniConstraintStream<S, A, E, F, Sc>
where Sc: Score,
{ /* private fields */ }
Expand description

Zero-erasure constraint stream over a single entity type.

UniConstraintStream accumulates filters and can be finalized into an IncrementalUniConstraint via penalize() or reward().

All type parameters are concrete - no trait objects, no Arc allocations in the hot path.

§Type Parameters

  • S - Solution type
  • A - Entity type
  • E - Extractor function type
  • F - Combined filter type
  • Sc - Score type

Implementations§

Source§

impl<S, A, E, Sc> UniConstraintStream<S, A, E, TrueFilter, Sc>
where S: Send + Sync + 'static, A: Clone + Send + Sync + 'static, E: Fn(&S) -> &[A] + Send + Sync, Sc: Score + 'static,

Source

pub fn new(extractor: E) -> Self

Creates a new uni-constraint stream with the given extractor.

Source§

impl<S, A, E, F, Sc> UniConstraintStream<S, A, E, F, Sc>
where S: Send + Sync + 'static, A: Clone + Send + Sync + 'static, E: Fn(&S) -> &[A] + Send + Sync, F: UniFilter<A>, Sc: Score + 'static,

Source

pub fn filter<P>( self, predicate: P, ) -> UniConstraintStream<S, A, E, AndUniFilter<F, FnUniFilter<P>>, Sc>
where P: Fn(&A) -> bool + Send + Sync,

Adds a filter predicate to the stream.

Multiple filters are combined with AND semantics at compile time. Each filter adds a new type layer, preserving zero-erasure.

Source

pub fn join_self<K, KA, KB>( self, joiner: EqualJoiner<KA, KB, K>, ) -> BiConstraintStream<S, A, K, E, KA, UniLeftBiFilter<F, A>, Sc>
where A: Hash + PartialEq, K: Eq + Hash + Clone + Send + Sync, KA: Fn(&A) -> K + Send + Sync, KB: Fn(&A) -> K + Send + Sync,

Joins this stream with itself to create pairs (zero-erasure).

Requires an EqualJoiner to enable key-based indexing for O(k) lookups. For self-joins, pairs are ordered (i < j) to avoid duplicates.

Any filters accumulated on this stream are applied to both entities individually before the join.

Source

pub fn join<B, EB, K, KA, KB>( self, extractor_b: EB, joiner: EqualJoiner<KA, KB, K>, ) -> CrossBiConstraintStream<S, A, B, K, E, EB, KA, KB, UniLeftBiFilter<F, B>, Sc>
where B: Clone + Send + Sync + 'static, EB: Fn(&S) -> &[B] + Send + Sync, K: Eq + Hash + Clone + Send + Sync, KA: Fn(&A) -> K + Send + Sync, KB: Fn(&B) -> K + Send + Sync,

Joins this stream with another collection to create cross-entity pairs (zero-erasure).

Requires an EqualJoiner to enable key-based indexing for O(1) lookups. Unlike join_self which pairs entities within the same collection, join creates pairs from two different collections (e.g., Shift joined with Employee).

Any filters accumulated on this stream are applied to the A entity before the join.

Source

pub fn group_by<K, KF, C>( self, key_fn: KF, collector: C, ) -> GroupedConstraintStream<S, A, K, E, KF, C, Sc>
where K: Clone + Eq + Hash + Send + Sync + 'static, KF: Fn(&A) -> K + Send + Sync, C: UniCollector<A> + Send + Sync + 'static, C::Accumulator: Send + Sync, C::Result: Clone + Send + Sync,

Groups entities by key and aggregates with a collector.

Returns a zero-erasure GroupedConstraintStream that can be penalized or rewarded based on the aggregated result for each group.

Source

pub fn balance<K, KF>( self, key_fn: KF, ) -> BalanceConstraintStream<S, A, K, E, F, KF, Sc>
where K: Clone + Eq + Hash + Send + Sync + 'static, KF: Fn(&A) -> Option<K> + Send + Sync,

Creates a balance constraint that penalizes uneven distribution across groups.

Unlike group_by which scores each group independently, balance computes a GLOBAL standard deviation across all group counts and produces a single score.

The key_fn returns Option<K> to allow skipping entities (e.g., unassigned shifts). Any filters accumulated on this stream are also applied.

§Example
use solverforge_scoring::stream::ConstraintFactory;
use solverforge_scoring::api::constraint_set::IncrementalConstraint;
use solverforge_core::score::SimpleScore;

#[derive(Clone)]
struct Shift { employee_id: Option<usize> }

#[derive(Clone)]
struct Solution { shifts: Vec<Shift> }

let constraint = ConstraintFactory::<Solution, SimpleScore>::new()
    .for_each(|s: &Solution| &s.shifts)
    .balance(|shift: &Shift| shift.employee_id)
    .penalize(SimpleScore::of(1000))
    .as_constraint("Balance workload");

let solution = Solution {
    shifts: vec![
        Shift { employee_id: Some(0) },
        Shift { employee_id: Some(0) },
        Shift { employee_id: Some(0) },
        Shift { employee_id: Some(1) },
    ],
};

// Employee 0: 3 shifts, Employee 1: 1 shift
// std_dev = 1.0, penalty = -1000
assert_eq!(constraint.evaluate(&solution), SimpleScore::of(-1000));
Source

pub fn if_exists_filtered<B, EB, K, KA, KB>( self, extractor_b: EB, joiner: EqualJoiner<KA, KB, K>, ) -> IfExistsStream<S, A, B, K, E, EB, KA, KB, F, Sc>
where B: Clone + Send + Sync + 'static, EB: Fn(&S) -> Vec<B> + Send + Sync, K: Eq + Hash + Clone + Send + Sync, KA: Fn(&A) -> K + Send + Sync, KB: Fn(&B) -> K + Send + Sync,

Filters A entities based on whether a matching B entity exists.

Use this when the B collection needs filtering (e.g., only vacationing employees). The extractor_b returns a Vec<B> to allow for filtering.

Any filters accumulated on this stream are applied to A entities.

§Example
use solverforge_scoring::stream::ConstraintFactory;
use solverforge_scoring::stream::joiner::equal_bi;
use solverforge_scoring::api::constraint_set::IncrementalConstraint;
use solverforge_core::score::SimpleScore;

#[derive(Clone)]
struct Shift { id: usize, employee_idx: Option<usize> }

#[derive(Clone)]
struct Employee { id: usize, on_vacation: bool }

#[derive(Clone)]
struct Schedule { shifts: Vec<Shift>, employees: Vec<Employee> }

// Penalize shifts assigned to employees who are on vacation
let constraint = ConstraintFactory::<Schedule, SimpleScore>::new()
    .for_each(|s: &Schedule| s.shifts.as_slice())
    .filter(|shift: &Shift| shift.employee_idx.is_some())
    .if_exists_filtered(
        |s: &Schedule| s.employees.iter().filter(|e| e.on_vacation).cloned().collect(),
        equal_bi(
            |shift: &Shift| shift.employee_idx,
            |emp: &Employee| Some(emp.id),
        ),
    )
    .penalize(SimpleScore::of(1))
    .as_constraint("Vacation conflict");

let schedule = Schedule {
    shifts: vec![
        Shift { id: 0, employee_idx: Some(0) },  // assigned to vacationing emp
        Shift { id: 1, employee_idx: Some(1) },  // assigned to working emp
        Shift { id: 2, employee_idx: None },     // unassigned (filtered out)
    ],
    employees: vec![
        Employee { id: 0, on_vacation: true },
        Employee { id: 1, on_vacation: false },
    ],
};

// Only shift 0 matches (assigned to employee 0 who is on vacation)
assert_eq!(constraint.evaluate(&schedule), SimpleScore::of(-1));
Source

pub fn if_not_exists_filtered<B, EB, K, KA, KB>( self, extractor_b: EB, joiner: EqualJoiner<KA, KB, K>, ) -> IfExistsStream<S, A, B, K, E, EB, KA, KB, F, Sc>
where B: Clone + Send + Sync + 'static, EB: Fn(&S) -> Vec<B> + Send + Sync, K: Eq + Hash + Clone + Send + Sync, KA: Fn(&A) -> K + Send + Sync, KB: Fn(&B) -> K + Send + Sync,

Filters A entities based on whether NO matching B entity exists.

Use this when the B collection needs filtering. The extractor_b returns a Vec<B> to allow for filtering.

Any filters accumulated on this stream are applied to A entities.

§Example
use solverforge_scoring::stream::ConstraintFactory;
use solverforge_scoring::stream::joiner::equal_bi;
use solverforge_scoring::api::constraint_set::IncrementalConstraint;
use solverforge_core::score::SimpleScore;

#[derive(Clone)]
struct Task { id: usize, assignee: Option<usize> }

#[derive(Clone)]
struct Worker { id: usize, available: bool }

#[derive(Clone)]
struct Schedule { tasks: Vec<Task>, workers: Vec<Worker> }

// Penalize tasks assigned to workers who are not available
let constraint = ConstraintFactory::<Schedule, SimpleScore>::new()
    .for_each(|s: &Schedule| s.tasks.as_slice())
    .filter(|task: &Task| task.assignee.is_some())
    .if_not_exists_filtered(
        |s: &Schedule| s.workers.iter().filter(|w| w.available).cloned().collect(),
        equal_bi(
            |task: &Task| task.assignee,
            |worker: &Worker| Some(worker.id),
        ),
    )
    .penalize(SimpleScore::of(1))
    .as_constraint("Unavailable worker");

let schedule = Schedule {
    tasks: vec![
        Task { id: 0, assignee: Some(0) },  // worker 0 is unavailable
        Task { id: 1, assignee: Some(1) },  // worker 1 is available
        Task { id: 2, assignee: None },     // unassigned (filtered out)
    ],
    workers: vec![
        Worker { id: 0, available: false },
        Worker { id: 1, available: true },
    ],
};

// Task 0's worker (id=0) is NOT in the available workers list
assert_eq!(constraint.evaluate(&schedule), SimpleScore::of(-1));
Source

pub fn penalize( self, weight: Sc, ) -> UniConstraintBuilder<S, A, E, F, impl Fn(&A) -> Sc + Send + Sync, Sc>
where Sc: Clone,

Penalizes each matching entity with a fixed weight.

Source

pub fn penalize_with<W>( self, weight_fn: W, ) -> UniConstraintBuilder<S, A, E, F, W, Sc>
where W: Fn(&A) -> Sc + Send + Sync,

Penalizes each matching entity with a dynamic weight.

Note: For dynamic weights, use penalize_hard_with to explicitly mark as a hard constraint, since the weight function cannot be evaluated at build time.

Source

pub fn penalize_hard_with<W>( self, weight_fn: W, ) -> UniConstraintBuilder<S, A, E, F, W, Sc>
where W: Fn(&A) -> Sc + Send + Sync,

Penalizes each matching entity with a dynamic weight, explicitly marked as a hard constraint.

Source

pub fn reward( self, weight: Sc, ) -> UniConstraintBuilder<S, A, E, F, impl Fn(&A) -> Sc + Send + Sync, Sc>
where Sc: Clone,

Rewards each matching entity with a fixed weight.

Source

pub fn reward_with<W>( self, weight_fn: W, ) -> UniConstraintBuilder<S, A, E, F, W, Sc>
where W: Fn(&A) -> Sc + Send + Sync,

Rewards each matching entity with a dynamic weight.

Note: For dynamic weights, use reward_hard_with to explicitly mark as a hard constraint, since the weight function cannot be evaluated at build time.

Source

pub fn reward_hard_with<W>( self, weight_fn: W, ) -> UniConstraintBuilder<S, A, E, F, W, Sc>
where W: Fn(&A) -> Sc + Send + Sync,

Rewards each matching entity with a dynamic weight, explicitly marked as a hard constraint.

Trait Implementations§

Source§

impl<S, A, E, F, Sc: Score> Debug for UniConstraintStream<S, A, E, F, Sc>

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more

Auto Trait Implementations§

§

impl<S, A, E, F, Sc> Freeze for UniConstraintStream<S, A, E, F, Sc>
where E: Freeze, F: Freeze,

§

impl<S, A, E, F, Sc> RefUnwindSafe for UniConstraintStream<S, A, E, F, Sc>

§

impl<S, A, E, F, Sc> Send for UniConstraintStream<S, A, E, F, Sc>
where E: Send, F: Send, S: Send, A: Send,

§

impl<S, A, E, F, Sc> Sync for UniConstraintStream<S, A, E, F, Sc>
where E: Sync, F: Sync, S: Sync, A: Sync,

§

impl<S, A, E, F, Sc> Unpin for UniConstraintStream<S, A, E, F, Sc>
where E: Unpin, F: Unpin, S: Unpin, A: Unpin, Sc: Unpin,

§

impl<S, A, E, F, Sc> UnwindSafe for UniConstraintStream<S, A, E, F, Sc>

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.