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, a: &A, b: &A, c: &A, d: &A, e: &A| {
125 filter.test(s, a, b, c, d) && joiner.matches(a, e)
126 };
127
128 PentaConstraintStream::new_self_join_with_filter(
129 self.extractor,
130 self.key_extractor,
131 FnPentaFilter::new(combined_filter),
132 )
133 }
134}
135
136// Additional doctests for individual methods
137
138#[cfg(doctest)]
139mod doctests {
140 /* # Filter method
141
142 ```
143 use solverforge_scoring::stream::ConstraintFactory;
144 use solverforge_scoring::stream::joiner::equal;
145 use solverforge_scoring::api::constraint_set::IncrementalConstraint;
146 use solverforge_core::score::SoftScore;
147
148 #[derive(Clone, Debug, Hash, PartialEq, Eq)]
149 struct Item { group: u32, value: i32 }
150
151 #[derive(Clone)]
152 struct Solution { items: Vec<Item> }
153
154 let constraint = ConstraintFactory::<Solution, SoftScore>::new()
155 .for_each(|s: &Solution| s.items.as_slice())
156 .join(equal(|i: &Item| i.group))
157 .join(equal(|i: &Item| i.group))
158 .join(equal(|i: &Item| i.group))
159 .filter(|a: &Item, b: &Item, c: &Item, d: &Item| {
160 a.value + b.value + c.value + d.value > 15
161 })
162 .penalize(SoftScore::of(1))
163 .named("High sum quadruples");
164
165 let solution = Solution {
166 items: vec![
167 Item { group: 1, value: 3 },
168 Item { group: 1, value: 4 },
169 Item { group: 1, value: 5 },
170 Item { group: 1, value: 6 },
171 ],
172 };
173
174 // 3+4+5+6=18 > 15, 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 .join(equal(|t: &Task| t.priority))
197 .penalize(SoftScore::of(5))
198 .named("Quadruple priority conflict");
199
200 let solution = Solution {
201 tasks: vec![
202 Task { priority: 1 },
203 Task { priority: 1 },
204 Task { priority: 1 },
205 Task { priority: 1 },
206 ],
207 };
208
209 // One quadruple = -5
210 assert_eq!(constraint.evaluate(&solution), SoftScore::of(-5));
211 ```
212
213 # named method
214
215 ```
216 use solverforge_scoring::stream::ConstraintFactory;
217 use solverforge_scoring::stream::joiner::equal;
218 use solverforge_scoring::api::constraint_set::IncrementalConstraint;
219 use solverforge_core::score::SoftScore;
220
221 #[derive(Clone, Debug, Hash, PartialEq, Eq)]
222 struct Item { id: usize }
223
224 #[derive(Clone)]
225 struct Solution { items: Vec<Item> }
226
227 let constraint = ConstraintFactory::<Solution, SoftScore>::new()
228 .for_each(|s: &Solution| s.items.as_slice())
229 .join(equal(|i: &Item| i.id))
230 .join(equal(|i: &Item| i.id))
231 .join(equal(|i: &Item| i.id))
232 .penalize(SoftScore::of(1))
233 .named("Quadruple items");
234
235 assert_eq!(constraint.name(), "Quadruple items");
236 ```
237 */
238}