Skip to main content

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::SoftScore;
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, SoftScore>::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(SoftScore::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), SoftScore::of(-1));
42// ```
43
44use std::hash::Hash;
45
46use solverforge_core::score::Score;
47
48use crate::constraint::IncrementalQuadConstraint;
49
50use super::filter::{FnPentaFilter, PentaFilter, 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(&S, &A, usize) -> K + Send + Sync,
69    F: QuadFilter<S, 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::SoftScore;
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, SoftScore>::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(SoftScore::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), SoftScore::of(-1));
111    // ```
112    pub fn join_self<J>(
113        self,
114        joiner: J,
115    ) -> PentaConstraintStream<S, A, K, E, KE, impl PentaFilter<S, 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 |s: &S, a: &A, b: &A, c: &A, d: &A, e: &A| {
122            filter.test(s, 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::SoftScore;
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, SoftScore>::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(SoftScore::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), SoftScore::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::SoftScore;
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, SoftScore>::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(SoftScore::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), SoftScore::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::SoftScore;
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, SoftScore>::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(SoftScore::of(1))
230    //     .as_constraint("Quadruple items");
231    //
232    // assert_eq!(constraint.name(), "Quadruple items");
233    // ```
234}