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}