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