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::IncrementalTriConstraint;
47
48use super::filter::{FnQuadFilter, QuadFilter, 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<S, 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 QuadFilter<S, 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 = move |s: &S, a: &A, b: &A, c: &A, d: &A| {
118 filter.test(s, a, b, c) && joiner.matches(a, d)
119 };
120
121 QuadConstraintStream::new_self_join_with_filter(
122 self.extractor,
123 self.key_extractor,
124 FnQuadFilter::new(combined_filter),
125 )
126 }
127}
128
129// Additional doctests for individual methods
130
131#[cfg(doctest)]
132mod doctests {
133 //! # Filter method
134 //!
135 //! ```
136 //! use solverforge_scoring::stream::ConstraintFactory;
137 //! use solverforge_scoring::stream::joiner::equal;
138 //! use solverforge_scoring::api::constraint_set::IncrementalConstraint;
139 //! use solverforge_core::score::SimpleScore;
140 //!
141 //! #[derive(Clone, Debug, Hash, PartialEq, Eq)]
142 //! struct Item { group: u32, value: i32 }
143 //!
144 //! #[derive(Clone)]
145 //! struct Solution { items: Vec<Item> }
146 //!
147 //! let constraint = ConstraintFactory::<Solution, SimpleScore>::new()
148 //! .for_each(|s: &Solution| s.items.as_slice())
149 //! .join_self(equal(|i: &Item| i.group))
150 //! .join_self(equal(|i: &Item| i.group))
151 //! .filter(|a: &Item, b: &Item, c: &Item| a.value + b.value + c.value > 10)
152 //! .penalize(SimpleScore::of(1))
153 //! .as_constraint("High sum triples");
154 //!
155 //! let solution = Solution {
156 //! items: vec![
157 //! Item { group: 1, value: 3 },
158 //! Item { group: 1, value: 4 },
159 //! Item { group: 1, value: 5 },
160 //! ],
161 //! };
162 //!
163 //! // 3+4+5=12 > 10, matches
164 //! assert_eq!(constraint.evaluate(&solution), SimpleScore::of(-1));
165 //! ```
166 //!
167 //! # Penalize method
168 //!
169 //! ```
170 //! use solverforge_scoring::stream::ConstraintFactory;
171 //! use solverforge_scoring::stream::joiner::equal;
172 //! use solverforge_scoring::api::constraint_set::IncrementalConstraint;
173 //! use solverforge_core::score::SimpleScore;
174 //!
175 //! #[derive(Clone, Debug, Hash, PartialEq, Eq)]
176 //! struct Task { priority: u32 }
177 //!
178 //! #[derive(Clone)]
179 //! struct Solution { tasks: Vec<Task> }
180 //!
181 //! let constraint = ConstraintFactory::<Solution, SimpleScore>::new()
182 //! .for_each(|s: &Solution| s.tasks.as_slice())
183 //! .join_self(equal(|t: &Task| t.priority))
184 //! .join_self(equal(|t: &Task| t.priority))
185 //! .penalize(SimpleScore::of(5))
186 //! .as_constraint("Triple priority conflict");
187 //!
188 //! let solution = Solution {
189 //! tasks: vec![
190 //! Task { priority: 1 },
191 //! Task { priority: 1 },
192 //! Task { priority: 1 },
193 //! ],
194 //! };
195 //!
196 //! // One triple = -5
197 //! assert_eq!(constraint.evaluate(&solution), SimpleScore::of(-5));
198 //! ```
199 //!
200 //! # Penalize with dynamic weight
201 //!
202 //! ```
203 //! use solverforge_scoring::stream::ConstraintFactory;
204 //! use solverforge_scoring::stream::joiner::equal;
205 //! use solverforge_scoring::api::constraint_set::IncrementalConstraint;
206 //! use solverforge_core::score::SimpleScore;
207 //!
208 //! #[derive(Clone, Debug, Hash, PartialEq, Eq)]
209 //! struct Task { team: u32, cost: i64 }
210 //!
211 //! #[derive(Clone)]
212 //! struct Solution { tasks: Vec<Task> }
213 //!
214 //! let constraint = ConstraintFactory::<Solution, SimpleScore>::new()
215 //! .for_each(|s: &Solution| s.tasks.as_slice())
216 //! .join_self(equal(|t: &Task| t.team))
217 //! .join_self(equal(|t: &Task| t.team))
218 //! .penalize_with(|a: &Task, b: &Task, c: &Task| {
219 //! SimpleScore::of(a.cost + b.cost + c.cost)
220 //! })
221 //! .as_constraint("Team cost");
222 //!
223 //! let solution = Solution {
224 //! tasks: vec![
225 //! Task { team: 1, cost: 2 },
226 //! Task { team: 1, cost: 3 },
227 //! Task { team: 1, cost: 5 },
228 //! ],
229 //! };
230 //!
231 //! // Penalty: 2+3+5 = -10
232 //! assert_eq!(constraint.evaluate(&solution), SimpleScore::of(-10));
233 //! ```
234 //!
235 //! # Reward method
236 //!
237 //! ```
238 //! use solverforge_scoring::stream::ConstraintFactory;
239 //! use solverforge_scoring::stream::joiner::equal;
240 //! use solverforge_scoring::api::constraint_set::IncrementalConstraint;
241 //! use solverforge_core::score::SimpleScore;
242 //!
243 //! #[derive(Clone, Debug, Hash, PartialEq, Eq)]
244 //! struct Person { team: u32 }
245 //!
246 //! #[derive(Clone)]
247 //! struct Solution { people: Vec<Person> }
248 //!
249 //! let constraint = ConstraintFactory::<Solution, SimpleScore>::new()
250 //! .for_each(|s: &Solution| s.people.as_slice())
251 //! .join_self(equal(|p: &Person| p.team))
252 //! .join_self(equal(|p: &Person| p.team))
253 //! .reward(SimpleScore::of(10))
254 //! .as_constraint("Team synergy");
255 //!
256 //! let solution = Solution {
257 //! people: vec![
258 //! Person { team: 1 },
259 //! Person { team: 1 },
260 //! Person { team: 1 },
261 //! ],
262 //! };
263 //!
264 //! // One triple = +10
265 //! assert_eq!(constraint.evaluate(&solution), SimpleScore::of(10));
266 //! ```
267 //!
268 //! # as_constraint method
269 //!
270 //! ```
271 //! use solverforge_scoring::stream::ConstraintFactory;
272 //! use solverforge_scoring::stream::joiner::equal;
273 //! use solverforge_scoring::api::constraint_set::IncrementalConstraint;
274 //! use solverforge_core::score::SimpleScore;
275 //!
276 //! #[derive(Clone, Debug, Hash, PartialEq, Eq)]
277 //! struct Item { id: usize }
278 //!
279 //! #[derive(Clone)]
280 //! struct Solution { items: Vec<Item> }
281 //!
282 //! let constraint = ConstraintFactory::<Solution, SimpleScore>::new()
283 //! .for_each(|s: &Solution| s.items.as_slice())
284 //! .join_self(equal(|i: &Item| i.id))
285 //! .join_self(equal(|i: &Item| i.id))
286 //! .penalize(SimpleScore::of(1))
287 //! .as_constraint("Triple items");
288 //!
289 //! assert_eq!(constraint.name(), "Triple items");
290 //! ```
291}