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