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::SimpleScore;
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, SimpleScore>::new()
23//! .for_each(|s: &Solution| s.tasks.as_slice())
24//! .join_self(equal(|t: &Task| t.team))
25//! .join_self(equal(|t: &Task| t.team))
26//! .join_self(equal(|t: &Task| t.team))
27//! .penalize(SimpleScore::of(1))
28//! .as_constraint("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), SimpleScore::of(-1));
42//! ```
43
44use std::hash::Hash;
45
46use solverforge_core::score::Score;
47
48use crate::constraint::quad_incremental::IncrementalQuadConstraint;
49
50use super::filter::{FnPentaFilter, 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_self 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(&A) -> K + Send + Sync,
69 F: QuadFilter<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::SimpleScore;
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, SimpleScore>::new()
90 /// .for_each(|s: &Solution| s.tasks.as_slice())
91 /// .join_self(equal(|t: &Task| t.team))
92 /// .join_self(equal(|t: &Task| t.team))
93 /// .join_self(equal(|t: &Task| t.team))
94 /// .join_self(equal(|t: &Task| t.team))
95 /// .penalize(SimpleScore::of(1))
96 /// .as_constraint("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), SimpleScore::of(-1));
111 /// ```
112 pub fn join_self<J>(
113 self,
114 joiner: J,
115 ) -> PentaConstraintStream<S, A, K, E, KE, impl super::filter::PentaFilter<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 |a: &A, b: &A, c: &A, d: &A, e: &A| {
122 filter.test(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::SimpleScore;
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, SimpleScore>::new()
152 //! .for_each(|s: &Solution| s.items.as_slice())
153 //! .join_self(equal(|i: &Item| i.group))
154 //! .join_self(equal(|i: &Item| i.group))
155 //! .join_self(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(SimpleScore::of(1))
160 //! .as_constraint("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), SimpleScore::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::SimpleScore;
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, SimpleScore>::new()
190 //! .for_each(|s: &Solution| s.tasks.as_slice())
191 //! .join_self(equal(|t: &Task| t.priority))
192 //! .join_self(equal(|t: &Task| t.priority))
193 //! .join_self(equal(|t: &Task| t.priority))
194 //! .penalize(SimpleScore::of(5))
195 //! .as_constraint("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), SimpleScore::of(-5));
208 //! ```
209 //!
210 //! # as_constraint 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::SimpleScore;
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, SimpleScore>::new()
225 //! .for_each(|s: &Solution| s.items.as_slice())
226 //! .join_self(equal(|i: &Item| i.id))
227 //! .join_self(equal(|i: &Item| i.id))
228 //! .join_self(equal(|i: &Item| i.id))
229 //! .penalize(SimpleScore::of(1))
230 //! .as_constraint("Quadruple items");
231 //!
232 //! assert_eq!(constraint.name(), "Quadruple items");
233 //! ```
234}