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