Skip to main content

solverforge_scoring/stream/
complemented_stream.rs

1/* Zero-erasure complemented constraint stream.
2
3A `ComplementedConstraintStream` adds entities from a complement source
4that are not present in grouped results, with default values.
5*/
6
7use std::hash::Hash;
8use std::marker::PhantomData;
9
10use solverforge_core::score::Score;
11use solverforge_core::{ConstraintRef, ImpactType};
12
13use super::collection_extract::CollectionExtract;
14use super::collector::{Accumulator, Collector};
15use super::relational::leaf_collector::{leaf_key, LeafCollector, OptionalEntityKey};
16use super::relational::operator::{
17    CollectionNode, ComplementNode, ComplementView, GroupNode, GroupView,
18};
19use super::relational::view_plan::{GroupKey, ViewEqualPlan};
20use super::relational::Leaf;
21use super::weighting_support::ConstraintWeight;
22use crate::constraint::relational::OperatorTerminal;
23
24/* Zero-erasure constraint stream with complemented groups.
25
26`ComplementedConstraintStream` results from calling `complement` on a
27`GroupedConstraintStream`. It ensures all keys from a complement source
28are represented, using default values for missing keys.
29
30The key function for A entities returns `Option<K>` to allow skipping
31entities without valid keys (e.g., unassigned shifts).
32
33# Type Parameters
34
35- `S` - Solution type
36- `A` - Original entity type (e.g., Shift)
37- `B` - Complement entity type (e.g., Employee)
38- `K` - Group key type
39- `EA` - Extractor for A entities
40- `EB` - Extractor for B entities (complement source)
41- `KA` - Key function for A (returns `Option<K>` to allow filtering)
42- `KB` - Key function for B
43- `C` - Collector type
44- `D` - Default value function
45- `Sc` - Score type
46
47# Example
48
49```
50use solverforge_scoring::stream::ConstraintFactory;
51use solverforge_scoring::stream::collector::count;
52use solverforge_scoring::api::constraint_set::IncrementalConstraint;
53use solverforge_core::score::SoftScore;
54
55#[derive(Clone, Hash, PartialEq, Eq)]
56struct Employee { id: usize }
57
58#[derive(Clone, Hash, PartialEq, Eq)]
59struct Shift { employee_id: usize }
60
61#[derive(Clone)]
62struct Schedule {
63employees: Vec<Employee>,
64shifts: Vec<Shift>,
65}
66
67// Count shifts per employee, including employees with 0 shifts
68let constraint = ConstraintFactory::<Schedule, SoftScore>::new()
69.for_each(|s: &Schedule| &s.shifts)
70.group_by(|shift: &Shift| shift.employee_id, count())
71.complement(
72|s: &Schedule| s.employees.as_slice(),
73|emp: &Employee| emp.id,
74|_emp: &Employee| 0usize,
75)
76.penalize(|_employee_id: &usize, count: &usize| SoftScore::of(*count as i64))
77.named("Shift count");
78
79let schedule = Schedule {
80employees: vec![Employee { id: 0 }, Employee { id: 1 }, Employee { id: 2 }],
81shifts: vec![
82Shift { employee_id: 0 },
83Shift { employee_id: 0 },
84// Employee 1 has 0 shifts, Employee 2 has 0 shifts
85],
86};
87
88// Employee 0: 2, Employee 1: 0, Employee 2: 0 → Total: -2
89assert_eq!(constraint.evaluate(&schedule), SoftScore::of(-2));
90```
91*/
92pub struct ComplementedConstraintStream<S, A, B, K, EA, EB, KA, KB, C, V, R, Acc, D, Sc>
93where
94    Sc: Score,
95{
96    extractor_a: EA,
97    extractor_b: EB,
98    key_a: KA,
99    key_b: KB,
100    collector: C,
101    default_fn: D,
102    _phantom: PhantomData<(
103        fn() -> S,
104        fn() -> A,
105        fn() -> B,
106        fn() -> K,
107        fn() -> V,
108        fn() -> R,
109        fn() -> Acc,
110        fn() -> Sc,
111    )>,
112}
113
114impl<S, A, B, K, EA, EB, KA, KB, C, V, R, Acc, D, Sc>
115    ComplementedConstraintStream<S, A, B, K, EA, EB, KA, KB, C, V, R, Acc, D, Sc>
116where
117    S: Send + Sync + 'static,
118    A: Clone + Send + Sync + 'static,
119    B: Clone + Send + Sync + 'static,
120    K: Clone + Eq + Hash + Send + Sync + 'static,
121    EA: CollectionExtract<S, Item = A>,
122    EB: CollectionExtract<S, Item = B>,
123    KA: Fn(&A) -> Option<K> + Send + Sync,
124    KB: Fn(&B) -> K + Send + Sync,
125    C: for<'i> Collector<&'i A, Value = V, Result = R, Accumulator = Acc> + Send + Sync + 'static,
126    V: Send + Sync + 'static,
127    R: Send + Sync + 'static,
128    Acc: Accumulator<V, R> + Send + Sync + 'static,
129    D: Fn(&B) -> R + Send + Sync,
130    Sc: Score + 'static,
131{
132    fn into_weighted_builder<W>(
133        self,
134        impact_type: ImpactType,
135        weight_fn: W,
136        is_hard: bool,
137    ) -> ComplementedConstraintBuilder<S, A, B, K, EA, EB, KA, KB, C, V, R, Acc, D, W, Sc>
138    where
139        W: Fn(&K, &R) -> Sc + Send + Sync,
140    {
141        ComplementedConstraintBuilder {
142            extractor_a: self.extractor_a,
143            extractor_b: self.extractor_b,
144            key_a: self.key_a,
145            key_b: self.key_b,
146            collector: self.collector,
147            default_fn: self.default_fn,
148            impact_type,
149            weight_fn,
150            is_hard,
151            _phantom: PhantomData,
152        }
153    }
154
155    // Creates a new complemented constraint stream.
156    pub(crate) fn new(
157        extractor_a: EA,
158        extractor_b: EB,
159        key_a: KA,
160        key_b: KB,
161        collector: C,
162        default_fn: D,
163    ) -> Self {
164        Self {
165            extractor_a,
166            extractor_b,
167            key_a,
168            key_b,
169            collector,
170            default_fn,
171            _phantom: PhantomData,
172        }
173    }
174
175    pub fn penalize<W>(
176        self,
177        weight: W,
178    ) -> ComplementedConstraintBuilder<
179        S,
180        A,
181        B,
182        K,
183        EA,
184        EB,
185        KA,
186        KB,
187        C,
188        V,
189        R,
190        Acc,
191        D,
192        impl Fn(&K, &R) -> Sc + Send + Sync,
193        Sc,
194    >
195    where
196        W: for<'w> ConstraintWeight<(&'w K, &'w R), Sc> + Send + Sync,
197    {
198        let is_hard = weight.is_hard();
199        self.into_weighted_builder(
200            ImpactType::Penalty,
201            move |key: &K, result: &R| weight.score((key, result)),
202            is_hard,
203        )
204    }
205
206    pub fn reward<W>(
207        self,
208        weight: W,
209    ) -> ComplementedConstraintBuilder<
210        S,
211        A,
212        B,
213        K,
214        EA,
215        EB,
216        KA,
217        KB,
218        C,
219        V,
220        R,
221        Acc,
222        D,
223        impl Fn(&K, &R) -> Sc + Send + Sync,
224        Sc,
225    >
226    where
227        W: for<'w> ConstraintWeight<(&'w K, &'w R), Sc> + Send + Sync,
228    {
229        let is_hard = weight.is_hard();
230        self.into_weighted_builder(
231            ImpactType::Reward,
232            move |key: &K, result: &R| weight.score((key, result)),
233            is_hard,
234        )
235    }
236}
237
238impl<S, A, B, K, EA, EB, KA, KB, C, V, R, Acc, D, Sc: Score> std::fmt::Debug
239    for ComplementedConstraintStream<S, A, B, K, EA, EB, KA, KB, C, V, R, Acc, D, Sc>
240{
241    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
242        f.debug_struct("ComplementedConstraintStream").finish()
243    }
244}
245
246// Zero-erasure builder for finalizing a complemented constraint.
247pub struct ComplementedConstraintBuilder<S, A, B, K, EA, EB, KA, KB, C, V, R, Acc, D, W, Sc>
248where
249    Sc: Score,
250{
251    extractor_a: EA,
252    extractor_b: EB,
253    key_a: KA,
254    key_b: KB,
255    collector: C,
256    default_fn: D,
257    impact_type: ImpactType,
258    weight_fn: W,
259    is_hard: bool,
260    _phantom: PhantomData<(
261        fn() -> S,
262        fn() -> A,
263        fn() -> B,
264        fn() -> K,
265        fn() -> V,
266        fn() -> R,
267        fn() -> Acc,
268        fn() -> Sc,
269    )>,
270}
271
272impl<S, A, B, K, EA, EB, KA, KB, C, V, R, Acc, D, W, Sc>
273    ComplementedConstraintBuilder<S, A, B, K, EA, EB, KA, KB, C, V, R, Acc, D, W, Sc>
274where
275    S: Send + Sync + 'static,
276    A: Clone + Send + Sync + 'static,
277    B: Clone + Send + Sync + 'static,
278    K: Clone + Eq + Hash + Send + Sync + 'static,
279    EA: CollectionExtract<S, Item = A> + 'static,
280    EB: CollectionExtract<S, Item = B> + 'static,
281    KA: Fn(&A) -> Option<K> + Send + Sync + 'static,
282    KB: Fn(&B) -> K + Send + Sync + Clone + 'static,
283    C: for<'i> Collector<&'i A, Value = V, Result = R, Accumulator = Acc> + Send + Sync + 'static,
284    V: Send + Sync + 'static,
285    R: Send + Sync + 'static,
286    Acc: Accumulator<V, R> + Send + Sync + 'static,
287    D: Fn(&B) -> R + Send + Sync,
288    W: Fn(&K, &R) -> Sc + Send + Sync,
289    Sc: Score + 'static,
290{
291    pub fn named(
292        self,
293        name: &str,
294    ) -> OperatorTerminal<
295        S,
296        ComplementNode<
297            S,
298            CollectionNode<S, EB>,
299            GroupNode<
300                CollectionNode<S, EA>,
301                impl for<'l> Fn(&Leaf<'l, A>) -> Option<K> + Send + Sync,
302                LeafCollector<C, V, R, Acc>,
303                Option<K>,
304                Acc,
305                V,
306                R,
307            >,
308            ViewEqualPlan<Option<K>, OptionalEntityKey<KB>, GroupKey>,
309            impl for<'l> Fn(&S, &Leaf<'l, B>) -> R + Send + Sync,
310            R,
311        >,
312        impl for<'l> Fn(
313                &S,
314                &ComplementView<
315                    'l,
316                    Leaf<'l, B>,
317                    GroupView<'l, S, CollectionNode<S, EA>, Option<K>, Acc, V, R>,
318                    R,
319                >,
320            ) -> Sc
321            + Send
322            + Sync,
323        Sc,
324    > {
325        let key_a = self.key_a;
326        let key_b = self.key_b;
327        let key_b_weight = key_b.clone();
328        let default_fn = self.default_fn;
329        let weight_fn = self.weight_fn;
330        let group = GroupNode::new(
331            CollectionNode::new(self.extractor_a, 0),
332            leaf_key(key_a),
333            LeafCollector::new(self.collector),
334        );
335        let targets = CollectionNode::new(self.extractor_b, 1);
336        let tree = ComplementNode::new(
337            targets,
338            group,
339            ViewEqualPlan::new(OptionalEntityKey::new(key_b.clone()), GroupKey),
340            move |_s: &S, row: &Leaf<'_, B>| default_fn(row.entity),
341        );
342        let weight = move |_s: &S,
343                           row: &ComplementView<
344            '_,
345            Leaf<'_, B>,
346            GroupView<'_, S, CollectionNode<S, EA>, Option<K>, Acc, V, R>,
347            R,
348        >| match row {
349            ComplementView::Real(pair) => {
350                let key = key_b_weight(pair.left.entity);
351                pair.right.with_result(|r| weight_fn(&key, r))
352            }
353            ComplementView::Default(projected) => {
354                let key = key_b_weight(projected.input.entity);
355                weight_fn(&key, projected.value)
356            }
357        };
358        OperatorTerminal::new(
359            ConstraintRef::new("", name),
360            self.impact_type,
361            tree,
362            weight,
363            self.is_hard,
364        )
365    }
366
367    // Finalizes the builder into a `Grouped`.
368}
369
370impl<S, A, B, K, EA, EB, KA, KB, C, V, R, Acc, D, W, Sc: Score> std::fmt::Debug
371    for ComplementedConstraintBuilder<S, A, B, K, EA, EB, KA, KB, C, V, R, Acc, D, W, Sc>
372{
373    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
374        f.debug_struct("ComplementedConstraintBuilder")
375            .field("impact_type", &self.impact_type)
376            .finish()
377    }
378}