solverforge_scoring/stream/tri_stream.rs
1//! Zero-erasure tri-constraint stream for three-entity constraint patterns.
2//!
3//! A `TriConstraintStream` operates on triples 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 three 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//! .penalize(SimpleScore::of(1))
27//! .as_constraint("Team clustering");
28//!
29//! let solution = Solution {
30//! tasks: vec![
31//! Task { team: 1 },
32//! Task { team: 1 },
33//! Task { team: 1 },
34//! Task { team: 2 },
35//! ],
36//! };
37//!
38//! // One triple on team 1: (0, 1, 2) = -1 penalty
39//! assert_eq!(constraint.evaluate(&solution), SimpleScore::of(-1));
40//! ```
41
42use std::hash::Hash;
43
44use solverforge_core::score::Score;
45
46use crate::constraint::tri_incremental::IncrementalTriConstraint;
47
48use super::filter::{FnQuadFilter, TriFilter};
49use super::joiner::Joiner;
50use super::quad_stream::QuadConstraintStream;
51
52super::arity_stream_macros::impl_arity_stream!(
53 tri,
54 TriConstraintStream,
55 TriConstraintBuilder,
56 IncrementalTriConstraint
57);
58
59// join_self method - transitions to QuadConstraintStream
60impl<S, A, K, E, KE, F, Sc> TriConstraintStream<S, A, K, E, KE, F, Sc>
61where
62 S: Send + Sync + 'static,
63 A: Clone + Hash + PartialEq + Send + Sync + 'static,
64 K: Eq + Hash + Clone + Send + Sync,
65 E: Fn(&S) -> &[A] + Send + Sync,
66 KE: Fn(&A) -> K + Send + Sync,
67 F: TriFilter<A, A, A>,
68 Sc: Score + 'static,
69{
70 /// Joins this stream with a fourth element to create quadruples.
71 ///
72 /// # Example
73 ///
74 /// ```
75 /// use solverforge_scoring::stream::ConstraintFactory;
76 /// use solverforge_scoring::stream::joiner::equal;
77 /// use solverforge_scoring::api::constraint_set::IncrementalConstraint;
78 /// use solverforge_core::score::SimpleScore;
79 ///
80 /// #[derive(Clone, Debug, Hash, PartialEq, Eq)]
81 /// struct Task { team: u32 }
82 ///
83 /// #[derive(Clone)]
84 /// struct Solution { tasks: Vec<Task> }
85 ///
86 /// // Penalize when four tasks are on the same team
87 /// let constraint = ConstraintFactory::<Solution, SimpleScore>::new()
88 /// .for_each(|s: &Solution| s.tasks.as_slice())
89 /// .join_self(equal(|t: &Task| t.team))
90 /// .join_self(equal(|t: &Task| t.team))
91 /// .join_self(equal(|t: &Task| t.team))
92 /// .penalize(SimpleScore::of(1))
93 /// .as_constraint("Team clustering");
94 ///
95 /// let solution = Solution {
96 /// tasks: vec![
97 /// Task { team: 1 },
98 /// Task { team: 1 },
99 /// Task { team: 1 },
100 /// Task { team: 1 },
101 /// Task { team: 2 },
102 /// ],
103 /// };
104 ///
105 /// // One quadruple on team 1: (0, 1, 2, 3) = -1 penalty
106 /// assert_eq!(constraint.evaluate(&solution), SimpleScore::of(-1));
107 /// ```
108 pub fn join_self<J>(
109 self,
110 joiner: J,
111 ) -> QuadConstraintStream<S, A, K, E, KE, impl super::filter::QuadFilter<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 =
118 move |a: &A, b: &A, c: &A, d: &A| filter.test(a, b, c) && joiner.matches(a, d);
119
120 QuadConstraintStream::new_self_join_with_filter(
121 self.extractor,
122 self.key_extractor,
123 FnQuadFilter::new(combined_filter),
124 )
125 }
126}
127
128// Additional doctests for individual methods
129
130#[cfg(doctest)]
131mod doctests {
132 //! # Filter method
133 //!
134 //! ```
135 //! use solverforge_scoring::stream::ConstraintFactory;
136 //! use solverforge_scoring::stream::joiner::equal;
137 //! use solverforge_scoring::api::constraint_set::IncrementalConstraint;
138 //! use solverforge_core::score::SimpleScore;
139 //!
140 //! #[derive(Clone, Debug, Hash, PartialEq, Eq)]
141 //! struct Item { group: u32, value: i32 }
142 //!
143 //! #[derive(Clone)]
144 //! struct Solution { items: Vec<Item> }
145 //!
146 //! let constraint = ConstraintFactory::<Solution, SimpleScore>::new()
147 //! .for_each(|s: &Solution| s.items.as_slice())
148 //! .join_self(equal(|i: &Item| i.group))
149 //! .join_self(equal(|i: &Item| i.group))
150 //! .filter(|a: &Item, b: &Item, c: &Item| a.value + b.value + c.value > 10)
151 //! .penalize(SimpleScore::of(1))
152 //! .as_constraint("High sum triples");
153 //!
154 //! let solution = Solution {
155 //! items: vec![
156 //! Item { group: 1, value: 3 },
157 //! Item { group: 1, value: 4 },
158 //! Item { group: 1, value: 5 },
159 //! ],
160 //! };
161 //!
162 //! // 3+4+5=12 > 10, matches
163 //! assert_eq!(constraint.evaluate(&solution), SimpleScore::of(-1));
164 //! ```
165 //!
166 //! # Penalize method
167 //!
168 //! ```
169 //! use solverforge_scoring::stream::ConstraintFactory;
170 //! use solverforge_scoring::stream::joiner::equal;
171 //! use solverforge_scoring::api::constraint_set::IncrementalConstraint;
172 //! use solverforge_core::score::SimpleScore;
173 //!
174 //! #[derive(Clone, Debug, Hash, PartialEq, Eq)]
175 //! struct Task { priority: u32 }
176 //!
177 //! #[derive(Clone)]
178 //! struct Solution { tasks: Vec<Task> }
179 //!
180 //! let constraint = ConstraintFactory::<Solution, SimpleScore>::new()
181 //! .for_each(|s: &Solution| s.tasks.as_slice())
182 //! .join_self(equal(|t: &Task| t.priority))
183 //! .join_self(equal(|t: &Task| t.priority))
184 //! .penalize(SimpleScore::of(5))
185 //! .as_constraint("Triple priority conflict");
186 //!
187 //! let solution = Solution {
188 //! tasks: vec![
189 //! Task { priority: 1 },
190 //! Task { priority: 1 },
191 //! Task { priority: 1 },
192 //! ],
193 //! };
194 //!
195 //! // One triple = -5
196 //! assert_eq!(constraint.evaluate(&solution), SimpleScore::of(-5));
197 //! ```
198 //!
199 //! # Penalize with dynamic weight
200 //!
201 //! ```
202 //! use solverforge_scoring::stream::ConstraintFactory;
203 //! use solverforge_scoring::stream::joiner::equal;
204 //! use solverforge_scoring::api::constraint_set::IncrementalConstraint;
205 //! use solverforge_core::score::SimpleScore;
206 //!
207 //! #[derive(Clone, Debug, Hash, PartialEq, Eq)]
208 //! struct Task { team: u32, cost: i64 }
209 //!
210 //! #[derive(Clone)]
211 //! struct Solution { tasks: Vec<Task> }
212 //!
213 //! let constraint = ConstraintFactory::<Solution, SimpleScore>::new()
214 //! .for_each(|s: &Solution| s.tasks.as_slice())
215 //! .join_self(equal(|t: &Task| t.team))
216 //! .join_self(equal(|t: &Task| t.team))
217 //! .penalize_with(|a: &Task, b: &Task, c: &Task| {
218 //! SimpleScore::of(a.cost + b.cost + c.cost)
219 //! })
220 //! .as_constraint("Team cost");
221 //!
222 //! let solution = Solution {
223 //! tasks: vec![
224 //! Task { team: 1, cost: 2 },
225 //! Task { team: 1, cost: 3 },
226 //! Task { team: 1, cost: 5 },
227 //! ],
228 //! };
229 //!
230 //! // Penalty: 2+3+5 = -10
231 //! assert_eq!(constraint.evaluate(&solution), SimpleScore::of(-10));
232 //! ```
233 //!
234 //! # Reward method
235 //!
236 //! ```
237 //! use solverforge_scoring::stream::ConstraintFactory;
238 //! use solverforge_scoring::stream::joiner::equal;
239 //! use solverforge_scoring::api::constraint_set::IncrementalConstraint;
240 //! use solverforge_core::score::SimpleScore;
241 //!
242 //! #[derive(Clone, Debug, Hash, PartialEq, Eq)]
243 //! struct Person { team: u32 }
244 //!
245 //! #[derive(Clone)]
246 //! struct Solution { people: Vec<Person> }
247 //!
248 //! let constraint = ConstraintFactory::<Solution, SimpleScore>::new()
249 //! .for_each(|s: &Solution| s.people.as_slice())
250 //! .join_self(equal(|p: &Person| p.team))
251 //! .join_self(equal(|p: &Person| p.team))
252 //! .reward(SimpleScore::of(10))
253 //! .as_constraint("Team synergy");
254 //!
255 //! let solution = Solution {
256 //! people: vec![
257 //! Person { team: 1 },
258 //! Person { team: 1 },
259 //! Person { team: 1 },
260 //! ],
261 //! };
262 //!
263 //! // One triple = +10
264 //! assert_eq!(constraint.evaluate(&solution), SimpleScore::of(10));
265 //! ```
266 //!
267 //! # as_constraint method
268 //!
269 //! ```
270 //! use solverforge_scoring::stream::ConstraintFactory;
271 //! use solverforge_scoring::stream::joiner::equal;
272 //! use solverforge_scoring::api::constraint_set::IncrementalConstraint;
273 //! use solverforge_core::score::SimpleScore;
274 //!
275 //! #[derive(Clone, Debug, Hash, PartialEq, Eq)]
276 //! struct Item { id: usize }
277 //!
278 //! #[derive(Clone)]
279 //! struct Solution { items: Vec<Item> }
280 //!
281 //! let constraint = ConstraintFactory::<Solution, SimpleScore>::new()
282 //! .for_each(|s: &Solution| s.items.as_slice())
283 //! .join_self(equal(|i: &Item| i.id))
284 //! .join_self(equal(|i: &Item| i.id))
285 //! .penalize(SimpleScore::of(1))
286 //! .as_constraint("Triple items");
287 //!
288 //! assert_eq!(constraint.name(), "Triple items");
289 //! ```
290}