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