solverforge_scoring/stream/quad_stream.rs
1/* Zero-erasure quad-constraint stream for four-entity constraint patterns.
2
3A `QuadConstraintStream` operates on quadruples 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 four 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.join(equal(|t: &Task| t.team))
27.penalize(SoftScore::of(1))
28.named("Team clustering");
29
30let solution = Solution {
31tasks: vec![
32Task { team: 1 },
33Task { team: 1 },
34Task { team: 1 },
35Task { team: 1 },
36Task { team: 2 },
37],
38};
39
40// One quadruple on team 1: (0, 1, 2, 3) = -1 penalty
41assert_eq!(constraint.evaluate(&solution), SoftScore::of(-1));
42```
43*/
44
45use std::hash::Hash;
46
47use solverforge_core::score::Score;
48
49use super::collection_extract::CollectionExtract;
50use super::filter::{FnPentaFilter, PentaFilter, QuadFilter};
51use super::joiner::Joiner;
52use super::penta_stream::PentaConstraintStream;
53
54super::arity_stream_macros::impl_arity_stream!(quad, QuadConstraintStream, QuadConstraintBuilder);
55
56// join method - transitions to PentaConstraintStream
57impl<S, A, K, E, KE, F, Sc> QuadConstraintStream<S, A, K, E, KE, F, Sc>
58where
59 S: Send + Sync + 'static,
60 A: Clone + Hash + PartialEq + Send + Sync + 'static,
61 K: Eq + Hash + Clone + Send + Sync,
62 E: CollectionExtract<S, Item = A>,
63 KE: Fn(&S, &A, usize) -> K + Send + Sync,
64 F: QuadFilter<S, A, A, A, A>,
65 Sc: Score + 'static,
66{
67 /* Joins this stream with a fifth element to create quintuples.
68
69 # Example
70
71 ```
72 use solverforge_scoring::stream::ConstraintFactory;
73 use solverforge_scoring::stream::joiner::equal;
74 use solverforge_scoring::api::constraint_set::IncrementalConstraint;
75 use solverforge_core::score::SoftScore;
76
77 #[derive(Clone, Debug, Hash, PartialEq, Eq)]
78 struct Task { team: u32 }
79
80 #[derive(Clone)]
81 struct Solution { tasks: Vec<Task> }
82
83 // Penalize when five tasks are on the same team
84 let constraint = ConstraintFactory::<Solution, SoftScore>::new()
85 .for_each(|s: &Solution| s.tasks.as_slice())
86 .join(equal(|t: &Task| t.team))
87 .join(equal(|t: &Task| t.team))
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: 1 },
99 Task { team: 1 },
100 Task { team: 2 },
101 ],
102 };
103
104 // One quintuple on team 1: (0, 1, 2, 3, 4) = -1 penalty
105 assert_eq!(constraint.evaluate(&solution), SoftScore::of(-1));
106 ```
107 */
108 pub fn join<J>(
109 self,
110 joiner: J,
111 ) -> PentaConstraintStream<S, A, K, E, KE, impl PentaFilter<S, A, A, A, A, A>, Sc>
112 where
113 J: Joiner<A, A> + 'static,
114 F: 'static,
115 {
116 let filter = self.filter;
117 let combined_filter = move |s: &S,
118 a: &A,
119 b: &A,
120 c: &A,
121 d: &A,
122 e: &A,
123 a_idx: usize,
124 b_idx: usize,
125 c_idx: usize,
126 d_idx: usize,
127 _e_idx: usize| {
128 filter.test(s, a, b, c, d, a_idx, b_idx, c_idx, d_idx) && joiner.matches(a, e)
129 };
130
131 PentaConstraintStream::new_self_join_with_filter(
132 self.extractor,
133 self.key_extractor,
134 FnPentaFilter::new(combined_filter),
135 )
136 }
137}
138
139// Additional doctests for individual methods
140
141#[cfg(doctest)]
142mod doctests {
143 /* # Filter method
144
145 ```
146 use solverforge_scoring::stream::ConstraintFactory;
147 use solverforge_scoring::stream::joiner::equal;
148 use solverforge_scoring::api::constraint_set::IncrementalConstraint;
149 use solverforge_core::score::SoftScore;
150
151 #[derive(Clone, Debug, Hash, PartialEq, Eq)]
152 struct Item { group: u32, value: i32 }
153
154 #[derive(Clone)]
155 struct Solution { items: Vec<Item> }
156
157 let constraint = ConstraintFactory::<Solution, SoftScore>::new()
158 .for_each(|s: &Solution| s.items.as_slice())
159 .join(equal(|i: &Item| i.group))
160 .join(equal(|i: &Item| i.group))
161 .join(equal(|i: &Item| i.group))
162 .filter(|a: &Item, b: &Item, c: &Item, d: &Item| {
163 a.value + b.value + c.value + d.value > 15
164 })
165 .penalize(SoftScore::of(1))
166 .named("High sum quadruples");
167
168 let solution = Solution {
169 items: vec![
170 Item { group: 1, value: 3 },
171 Item { group: 1, value: 4 },
172 Item { group: 1, value: 5 },
173 Item { group: 1, value: 6 },
174 ],
175 };
176
177 // 3+4+5+6=18 > 15, matches
178 assert_eq!(constraint.evaluate(&solution), SoftScore::of(-1));
179 ```
180
181 # Penalize method
182
183 ```
184 use solverforge_scoring::stream::ConstraintFactory;
185 use solverforge_scoring::stream::joiner::equal;
186 use solverforge_scoring::api::constraint_set::IncrementalConstraint;
187 use solverforge_core::score::SoftScore;
188
189 #[derive(Clone, Debug, Hash, PartialEq, Eq)]
190 struct Task { priority: u32 }
191
192 #[derive(Clone)]
193 struct Solution { tasks: Vec<Task> }
194
195 let constraint = ConstraintFactory::<Solution, SoftScore>::new()
196 .for_each(|s: &Solution| s.tasks.as_slice())
197 .join(equal(|t: &Task| t.priority))
198 .join(equal(|t: &Task| t.priority))
199 .join(equal(|t: &Task| t.priority))
200 .penalize(SoftScore::of(5))
201 .named("Quadruple priority conflict");
202
203 let solution = Solution {
204 tasks: vec![
205 Task { priority: 1 },
206 Task { priority: 1 },
207 Task { priority: 1 },
208 Task { priority: 1 },
209 ],
210 };
211
212 // One quadruple = -5
213 assert_eq!(constraint.evaluate(&solution), SoftScore::of(-5));
214 ```
215
216 # named method
217
218 ```
219 use solverforge_scoring::stream::ConstraintFactory;
220 use solverforge_scoring::stream::joiner::equal;
221 use solverforge_scoring::api::constraint_set::IncrementalConstraint;
222 use solverforge_core::score::SoftScore;
223
224 #[derive(Clone, Debug, Hash, PartialEq, Eq)]
225 struct Item { id: usize }
226
227 #[derive(Clone)]
228 struct Solution { items: Vec<Item> }
229
230 let constraint = ConstraintFactory::<Solution, SoftScore>::new()
231 .for_each(|s: &Solution| s.items.as_slice())
232 .join(equal(|i: &Item| i.id))
233 .join(equal(|i: &Item| i.id))
234 .join(equal(|i: &Item| i.id))
235 .penalize(SoftScore::of(1))
236 .named("Quadruple items");
237
238 assert_eq!(constraint.name(), "Quadruple items");
239 ```
240 */
241}