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}