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}