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