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}