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,
121 a: &A,
122 b: &A,
123 c: &A,
124 d: &A,
125 a_idx: usize,
126 b_idx: usize,
127 c_idx: usize,
128 _d_idx: usize| {
129 filter.test(s, a, b, c, a_idx, b_idx, c_idx) && joiner.matches(a, d)
130 };
131
132 QuadConstraintStream::new_self_join_with_filter(
133 self.extractor,
134 self.key_extractor,
135 FnQuadFilter::new(combined_filter),
136 )
137 }
138}
139
140// Additional doctests for individual methods
141
142#[cfg(doctest)]
143mod doctests {
144 /* # Filter method
145
146 ```
147 use solverforge_scoring::stream::ConstraintFactory;
148 use solverforge_scoring::stream::joiner::equal;
149 use solverforge_scoring::api::constraint_set::IncrementalConstraint;
150 use solverforge_core::score::SoftScore;
151
152 #[derive(Clone, Debug, Hash, PartialEq, Eq)]
153 struct Item { group: u32, value: i32 }
154
155 #[derive(Clone)]
156 struct Solution { items: Vec<Item> }
157
158 let constraint = ConstraintFactory::<Solution, SoftScore>::new()
159 .for_each(|s: &Solution| s.items.as_slice())
160 .join(equal(|i: &Item| i.group))
161 .join(equal(|i: &Item| i.group))
162 .filter(|a: &Item, b: &Item, c: &Item| a.value + b.value + c.value > 10)
163 .penalize(SoftScore::of(1))
164 .named("High sum triples");
165
166 let solution = Solution {
167 items: vec![
168 Item { group: 1, value: 3 },
169 Item { group: 1, value: 4 },
170 Item { group: 1, value: 5 },
171 ],
172 };
173
174 // 3+4+5=12 > 10, matches
175 assert_eq!(constraint.evaluate(&solution), SoftScore::of(-1));
176 ```
177
178 # Penalize method
179
180 ```
181 use solverforge_scoring::stream::ConstraintFactory;
182 use solverforge_scoring::stream::joiner::equal;
183 use solverforge_scoring::api::constraint_set::IncrementalConstraint;
184 use solverforge_core::score::SoftScore;
185
186 #[derive(Clone, Debug, Hash, PartialEq, Eq)]
187 struct Task { priority: u32 }
188
189 #[derive(Clone)]
190 struct Solution { tasks: Vec<Task> }
191
192 let constraint = ConstraintFactory::<Solution, SoftScore>::new()
193 .for_each(|s: &Solution| s.tasks.as_slice())
194 .join(equal(|t: &Task| t.priority))
195 .join(equal(|t: &Task| t.priority))
196 .penalize(SoftScore::of(5))
197 .named("Triple priority conflict");
198
199 let solution = Solution {
200 tasks: vec![
201 Task { priority: 1 },
202 Task { priority: 1 },
203 Task { priority: 1 },
204 ],
205 };
206
207 // One triple = -5
208 assert_eq!(constraint.evaluate(&solution), SoftScore::of(-5));
209 ```
210
211 # Penalize with dynamic weight
212
213 ```
214 use solverforge_scoring::stream::ConstraintFactory;
215 use solverforge_scoring::stream::joiner::equal;
216 use solverforge_scoring::api::constraint_set::IncrementalConstraint;
217 use solverforge_core::score::SoftScore;
218
219 #[derive(Clone, Debug, Hash, PartialEq, Eq)]
220 struct Task { team: u32, cost: i64 }
221
222 #[derive(Clone)]
223 struct Solution { tasks: Vec<Task> }
224
225 let constraint = ConstraintFactory::<Solution, SoftScore>::new()
226 .for_each(|s: &Solution| s.tasks.as_slice())
227 .join(equal(|t: &Task| t.team))
228 .join(equal(|t: &Task| t.team))
229 .penalize(|a: &Task, b: &Task, c: &Task| {
230 SoftScore::of(a.cost + b.cost + c.cost)
231 })
232 .named("Team cost");
233
234 let solution = Solution {
235 tasks: vec![
236 Task { team: 1, cost: 2 },
237 Task { team: 1, cost: 3 },
238 Task { team: 1, cost: 5 },
239 ],
240 };
241
242 // Penalty: 2+3+5 = -10
243 assert_eq!(constraint.evaluate(&solution), SoftScore::of(-10));
244 ```
245
246 # Reward method
247
248 ```
249 use solverforge_scoring::stream::ConstraintFactory;
250 use solverforge_scoring::stream::joiner::equal;
251 use solverforge_scoring::api::constraint_set::IncrementalConstraint;
252 use solverforge_core::score::SoftScore;
253
254 #[derive(Clone, Debug, Hash, PartialEq, Eq)]
255 struct Person { team: u32 }
256
257 #[derive(Clone)]
258 struct Solution { people: Vec<Person> }
259
260 let constraint = ConstraintFactory::<Solution, SoftScore>::new()
261 .for_each(|s: &Solution| s.people.as_slice())
262 .join(equal(|p: &Person| p.team))
263 .join(equal(|p: &Person| p.team))
264 .reward(SoftScore::of(10))
265 .named("Team synergy");
266
267 let solution = Solution {
268 people: vec![
269 Person { team: 1 },
270 Person { team: 1 },
271 Person { team: 1 },
272 ],
273 };
274
275 // One triple = +10
276 assert_eq!(constraint.evaluate(&solution), SoftScore::of(10));
277 ```
278
279 # named method
280
281 ```
282 use solverforge_scoring::stream::ConstraintFactory;
283 use solverforge_scoring::stream::joiner::equal;
284 use solverforge_scoring::api::constraint_set::IncrementalConstraint;
285 use solverforge_core::score::SoftScore;
286
287 #[derive(Clone, Debug, Hash, PartialEq, Eq)]
288 struct Item { id: usize }
289
290 #[derive(Clone)]
291 struct Solution { items: Vec<Item> }
292
293 let constraint = ConstraintFactory::<Solution, SoftScore>::new()
294 .for_each(|s: &Solution| s.items.as_slice())
295 .join(equal(|i: &Item| i.id))
296 .join(equal(|i: &Item| i.id))
297 .penalize(SoftScore::of(1))
298 .named("Triple items");
299
300 assert_eq!(constraint.name(), "Triple items");
301 ```
302 */
303}