Skip to main content

solverforge_scoring/stream/
bi_stream.rs

1/* Zero-erasure bi-constraint stream for self-join patterns.
2
3A `BiConstraintStream` operates on pairs of entities from the same
4collection (self-join), such as comparing Shifts to each other.
5All type information is preserved at compile time - no Arc, no dyn.
6
7# Example
8
9```
10use solverforge_scoring::stream::ConstraintFactory;
11use solverforge_scoring::stream::joiner::equal;
12use solverforge_scoring::api::constraint_set::IncrementalConstraint;
13use solverforge_core::score::SoftScore;
14
15#[derive(Clone, Debug, Hash, PartialEq, Eq)]
16struct Task { team: u32 }
17
18#[derive(Clone)]
19struct Solution { tasks: Vec<Task> }
20
21// Penalize when two tasks are on the same team
22let constraint = ConstraintFactory::<Solution, SoftScore>::new()
23.for_each(|s: &Solution| s.tasks.as_slice())
24.join(equal(|t: &Task| t.team))
25.penalize(SoftScore::of(1))
26.named("Team conflict");
27
28let solution = Solution {
29tasks: vec![
30Task { team: 1 },
31Task { team: 1 },
32Task { team: 2 },
33],
34};
35
36// One pair on team 1: (0, 1) = -1 penalty
37assert_eq!(constraint.evaluate(&solution), SoftScore::of(-1));
38```
39*/
40
41use std::hash::Hash;
42
43use solverforge_core::score::Score;
44
45use super::filter::{BiFilter, FnTriFilter, TriFilter};
46use super::joiner::Joiner;
47use super::tri_stream::TriConstraintStream;
48
49super::arity_stream_macros::impl_arity_stream!(bi, BiConstraintStream, BiConstraintBuilder);
50
51// join method - transitions to TriConstraintStream
52impl<S, A, K, E, KE, F, Sc> BiConstraintStream<S, A, K, E, KE, F, Sc>
53where
54    S: Send + Sync + 'static,
55    A: Clone + Hash + PartialEq + Send + Sync + 'static,
56    K: Eq + Hash + Clone + Send + Sync,
57    E: super::collection_extract::CollectionExtract<S, Item = A>,
58    KE: Fn(&S, &A, usize) -> K + Send + Sync,
59    F: BiFilter<S, A, A>,
60    Sc: Score + 'static,
61{
62    /* Joins this stream with a third element to create triples.
63
64    # Example
65
66    ```
67    use solverforge_scoring::stream::ConstraintFactory;
68    use solverforge_scoring::stream::joiner::equal;
69    use solverforge_scoring::api::constraint_set::IncrementalConstraint;
70    use solverforge_core::score::SoftScore;
71
72    #[derive(Clone, Debug, Hash, PartialEq, Eq)]
73    struct Task { team: u32 }
74
75    #[derive(Clone)]
76    struct Solution { tasks: Vec<Task> }
77
78    // Penalize when three tasks are on the same team
79    let constraint = ConstraintFactory::<Solution, SoftScore>::new()
80    .for_each(|s: &Solution| s.tasks.as_slice())
81    .join(equal(|t: &Task| t.team))
82    .join(equal(|t: &Task| t.team))
83    .penalize(SoftScore::of(1))
84    .named("Team clustering");
85
86    let solution = Solution {
87    tasks: vec![
88    Task { team: 1 },
89    Task { team: 1 },
90    Task { team: 1 },
91    Task { team: 2 },
92    ],
93    };
94
95    // One triple on team 1: (0, 1, 2) = -1 penalty
96    assert_eq!(constraint.evaluate(&solution), SoftScore::of(-1));
97    ```
98    */
99    pub fn join<J>(
100        self,
101        joiner: J,
102    ) -> TriConstraintStream<S, A, K, E, KE, impl TriFilter<S, A, A, A>, Sc>
103    where
104        J: Joiner<A, A> + 'static,
105        F: 'static,
106    {
107        let filter = self.filter;
108        let combined_filter =
109            move |s: &S, a: &A, b: &A, c: &A, a_idx: usize, b_idx: usize, _c_idx: usize| {
110                filter.test(s, a, b, a_idx, b_idx) && joiner.matches(a, c)
111            };
112
113        TriConstraintStream::new_self_join_with_filter(
114            self.extractor,
115            self.key_extractor,
116            FnTriFilter::new(combined_filter),
117        )
118    }
119}
120
121// Additional doctests for individual methods
122
123#[cfg(doctest)]
124mod doctests {
125    /* # Filter method
126
127    ```
128    use solverforge_scoring::stream::ConstraintFactory;
129    use solverforge_scoring::stream::joiner::equal;
130    use solverforge_scoring::api::constraint_set::IncrementalConstraint;
131    use solverforge_core::score::SoftScore;
132
133    #[derive(Clone, Debug, Hash, PartialEq, Eq)]
134    struct Item { group: u32, value: i32 }
135
136    #[derive(Clone)]
137    struct Solution { items: Vec<Item> }
138
139    let constraint = ConstraintFactory::<Solution, SoftScore>::new()
140    .for_each(|s: &Solution| s.items.as_slice())
141    .join(equal(|i: &Item| i.group))
142    .filter(|a: &Item, b: &Item| a.value + b.value > 10)
143    .penalize(SoftScore::of(1))
144    .named("High sum pairs");
145
146    let solution = Solution {
147    items: vec![
148    Item { group: 1, value: 6 },
149    Item { group: 1, value: 7 },
150    ],
151    };
152
153    // 6+7=13 > 10, matches
154    assert_eq!(constraint.evaluate(&solution), SoftScore::of(-1));
155    ```
156
157    # Penalize method
158
159    ```
160    use solverforge_scoring::stream::ConstraintFactory;
161    use solverforge_scoring::stream::joiner::equal;
162    use solverforge_scoring::api::constraint_set::IncrementalConstraint;
163    use solverforge_core::score::SoftScore;
164
165    #[derive(Clone, Debug, Hash, PartialEq, Eq)]
166    struct Task { priority: u32 }
167
168    #[derive(Clone)]
169    struct Solution { tasks: Vec<Task> }
170
171    let constraint = ConstraintFactory::<Solution, SoftScore>::new()
172    .for_each(|s: &Solution| s.tasks.as_slice())
173    .join(equal(|t: &Task| t.priority))
174    .penalize(SoftScore::of(5))
175    .named("Pair priority conflict");
176
177    let solution = Solution {
178    tasks: vec![
179    Task { priority: 1 },
180    Task { priority: 1 },
181    ],
182    };
183
184    // One pair = -5
185    assert_eq!(constraint.evaluate(&solution), SoftScore::of(-5));
186    ```
187
188    # Penalize with dynamic weight
189
190    ```
191    use solverforge_scoring::stream::ConstraintFactory;
192    use solverforge_scoring::stream::joiner::equal;
193    use solverforge_scoring::api::constraint_set::IncrementalConstraint;
194    use solverforge_core::score::SoftScore;
195
196    #[derive(Clone, Debug, Hash, PartialEq, Eq)]
197    struct Task { team: u32, cost: i64 }
198
199    #[derive(Clone)]
200    struct Solution { tasks: Vec<Task> }
201
202    let constraint = ConstraintFactory::<Solution, SoftScore>::new()
203    .for_each(|s: &Solution| s.tasks.as_slice())
204    .join(equal(|t: &Task| t.team))
205    .penalize(|a: &Task, b: &Task| {
206    SoftScore::of(a.cost + b.cost)
207    })
208    .named("Team cost");
209
210    let solution = Solution {
211    tasks: vec![
212    Task { team: 1, cost: 2 },
213    Task { team: 1, cost: 3 },
214    ],
215    };
216
217    // Penalty: 2+3 = -5
218    assert_eq!(constraint.evaluate(&solution), SoftScore::of(-5));
219    ```
220
221    # Reward method
222
223    ```
224    use solverforge_scoring::stream::ConstraintFactory;
225    use solverforge_scoring::stream::joiner::equal;
226    use solverforge_scoring::api::constraint_set::IncrementalConstraint;
227    use solverforge_core::score::SoftScore;
228
229    #[derive(Clone, Debug, Hash, PartialEq, Eq)]
230    struct Person { team: u32 }
231
232    #[derive(Clone)]
233    struct Solution { people: Vec<Person> }
234
235    let constraint = ConstraintFactory::<Solution, SoftScore>::new()
236    .for_each(|s: &Solution| s.people.as_slice())
237    .join(equal(|p: &Person| p.team))
238    .reward(SoftScore::of(10))
239    .named("Team synergy");
240
241    let solution = Solution {
242    people: vec![
243    Person { team: 1 },
244    Person { team: 1 },
245    ],
246    };
247
248    // One pair = +10
249    assert_eq!(constraint.evaluate(&solution), SoftScore::of(10));
250    ```
251
252    # named method
253
254    ```
255    use solverforge_scoring::stream::ConstraintFactory;
256    use solverforge_scoring::stream::joiner::equal;
257    use solverforge_scoring::api::constraint_set::IncrementalConstraint;
258    use solverforge_core::score::SoftScore;
259
260    #[derive(Clone, Debug, Hash, PartialEq, Eq)]
261    struct Item { id: usize }
262
263    #[derive(Clone)]
264    struct Solution { items: Vec<Item> }
265
266    let constraint = ConstraintFactory::<Solution, SoftScore>::new()
267    .for_each(|s: &Solution| s.items.as_slice())
268    .join(equal(|i: &Item| i.id))
269    .penalize(SoftScore::of(1))
270    .named("Pair items");
271
272    assert_eq!(constraint.name(), "Pair items");
273    ```
274    */
275}