Skip to main content

solverforge_scoring/stream/
quad_stream.rs

1/* Zero-erasure quad-constraint stream for four-entity constraint patterns.
2
3A `QuadConstraintStream` operates on quadruples of entities and supports
4filtering, weighting, and constraint finalization. All type information
5is preserved at compile time - no Arc, no dyn.
6
7# Example
8
9```
10use solverforge_scoring::stream::ConstraintFactory;
11use solverforge_scoring::stream::joiner::equal;
12use solverforge_scoring::api::constraint_set::IncrementalConstraint;
13use solverforge_core::score::SoftScore;
14
15#[derive(Clone, Debug, Hash, PartialEq, Eq)]
16struct Task { team: u32 }
17
18#[derive(Clone)]
19struct Solution { tasks: Vec<Task> }
20
21// Penalize when four tasks are on the same team
22let constraint = ConstraintFactory::<Solution, SoftScore>::new()
23.for_each(|s: &Solution| s.tasks.as_slice())
24.join(equal(|t: &Task| t.team))
25.join(equal(|t: &Task| t.team))
26.join(equal(|t: &Task| t.team))
27.penalize(SoftScore::of(1))
28.named("Team clustering");
29
30let solution = Solution {
31tasks: vec![
32Task { team: 1 },
33Task { team: 1 },
34Task { team: 1 },
35Task { team: 1 },
36Task { team: 2 },
37],
38};
39
40// One quadruple on team 1: (0, 1, 2, 3) = -1 penalty
41assert_eq!(constraint.evaluate(&solution), SoftScore::of(-1));
42```
43*/
44
45use std::hash::Hash;
46
47use solverforge_core::score::Score;
48
49use crate::constraint::IncrementalQuadConstraint;
50
51use super::collection_extract::CollectionExtract;
52use super::filter::{FnPentaFilter, PentaFilter, QuadFilter};
53use super::joiner::Joiner;
54use super::penta_stream::PentaConstraintStream;
55
56super::arity_stream_macros::impl_arity_stream!(
57    quad,
58    QuadConstraintStream,
59    QuadConstraintBuilder,
60    IncrementalQuadConstraint
61);
62
63// join method - transitions to PentaConstraintStream
64impl<S, A, K, E, KE, F, Sc> QuadConstraintStream<S, A, K, E, KE, F, Sc>
65where
66    S: Send + Sync + 'static,
67    A: Clone + Hash + PartialEq + Send + Sync + 'static,
68    K: Eq + Hash + Clone + Send + Sync,
69    E: CollectionExtract<S, Item = A>,
70    KE: Fn(&S, &A, usize) -> K + Send + Sync,
71    F: QuadFilter<S, A, A, A, A>,
72    Sc: Score + 'static,
73{
74    /* Joins this stream with a fifth element to create quintuples.
75
76    # Example
77
78    ```
79    use solverforge_scoring::stream::ConstraintFactory;
80    use solverforge_scoring::stream::joiner::equal;
81    use solverforge_scoring::api::constraint_set::IncrementalConstraint;
82    use solverforge_core::score::SoftScore;
83
84    #[derive(Clone, Debug, Hash, PartialEq, Eq)]
85    struct Task { team: u32 }
86
87    #[derive(Clone)]
88    struct Solution { tasks: Vec<Task> }
89
90    // Penalize when five tasks are on the same team
91    let constraint = ConstraintFactory::<Solution, SoftScore>::new()
92    .for_each(|s: &Solution| s.tasks.as_slice())
93    .join(equal(|t: &Task| t.team))
94    .join(equal(|t: &Task| t.team))
95    .join(equal(|t: &Task| t.team))
96    .join(equal(|t: &Task| t.team))
97    .penalize(SoftScore::of(1))
98    .named("Team clustering");
99
100    let solution = Solution {
101    tasks: vec![
102    Task { team: 1 },
103    Task { team: 1 },
104    Task { team: 1 },
105    Task { team: 1 },
106    Task { team: 1 },
107    Task { team: 2 },
108    ],
109    };
110
111    // One quintuple on team 1: (0, 1, 2, 3, 4) = -1 penalty
112    assert_eq!(constraint.evaluate(&solution), SoftScore::of(-1));
113    ```
114    */
115    pub fn join<J>(
116        self,
117        joiner: J,
118    ) -> PentaConstraintStream<S, A, K, E, KE, impl PentaFilter<S, A, A, A, A, A>, Sc>
119    where
120        J: Joiner<A, A> + 'static,
121        F: 'static,
122    {
123        let filter = self.filter;
124        let combined_filter = move |s: &S,
125                                    a: &A,
126                                    b: &A,
127                                    c: &A,
128                                    d: &A,
129                                    e: &A,
130                                    a_idx: usize,
131                                    b_idx: usize,
132                                    c_idx: usize,
133                                    d_idx: usize,
134                                    _e_idx: usize| {
135            filter.test(s, a, b, c, d, a_idx, b_idx, c_idx, d_idx) && joiner.matches(a, e)
136        };
137
138        PentaConstraintStream::new_self_join_with_filter(
139            self.extractor,
140            self.key_extractor,
141            FnPentaFilter::new(combined_filter),
142        )
143    }
144}
145
146// Additional doctests for individual methods
147
148#[cfg(doctest)]
149mod doctests {
150    /* # Filter method
151
152    ```
153    use solverforge_scoring::stream::ConstraintFactory;
154    use solverforge_scoring::stream::joiner::equal;
155    use solverforge_scoring::api::constraint_set::IncrementalConstraint;
156    use solverforge_core::score::SoftScore;
157
158    #[derive(Clone, Debug, Hash, PartialEq, Eq)]
159    struct Item { group: u32, value: i32 }
160
161    #[derive(Clone)]
162    struct Solution { items: Vec<Item> }
163
164    let constraint = ConstraintFactory::<Solution, SoftScore>::new()
165    .for_each(|s: &Solution| s.items.as_slice())
166    .join(equal(|i: &Item| i.group))
167    .join(equal(|i: &Item| i.group))
168    .join(equal(|i: &Item| i.group))
169    .filter(|a: &Item, b: &Item, c: &Item, d: &Item| {
170    a.value + b.value + c.value + d.value > 15
171    })
172    .penalize(SoftScore::of(1))
173    .named("High sum quadruples");
174
175    let solution = Solution {
176    items: vec![
177    Item { group: 1, value: 3 },
178    Item { group: 1, value: 4 },
179    Item { group: 1, value: 5 },
180    Item { group: 1, value: 6 },
181    ],
182    };
183
184    // 3+4+5+6=18 > 15, matches
185    assert_eq!(constraint.evaluate(&solution), SoftScore::of(-1));
186    ```
187
188    # Penalize method
189
190    ```
191    use solverforge_scoring::stream::ConstraintFactory;
192    use solverforge_scoring::stream::joiner::equal;
193    use solverforge_scoring::api::constraint_set::IncrementalConstraint;
194    use solverforge_core::score::SoftScore;
195
196    #[derive(Clone, Debug, Hash, PartialEq, Eq)]
197    struct Task { priority: u32 }
198
199    #[derive(Clone)]
200    struct Solution { tasks: Vec<Task> }
201
202    let constraint = ConstraintFactory::<Solution, SoftScore>::new()
203    .for_each(|s: &Solution| s.tasks.as_slice())
204    .join(equal(|t: &Task| t.priority))
205    .join(equal(|t: &Task| t.priority))
206    .join(equal(|t: &Task| t.priority))
207    .penalize(SoftScore::of(5))
208    .named("Quadruple priority conflict");
209
210    let solution = Solution {
211    tasks: vec![
212    Task { priority: 1 },
213    Task { priority: 1 },
214    Task { priority: 1 },
215    Task { priority: 1 },
216    ],
217    };
218
219    // One quadruple = -5
220    assert_eq!(constraint.evaluate(&solution), SoftScore::of(-5));
221    ```
222
223    # named method
224
225    ```
226    use solverforge_scoring::stream::ConstraintFactory;
227    use solverforge_scoring::stream::joiner::equal;
228    use solverforge_scoring::api::constraint_set::IncrementalConstraint;
229    use solverforge_core::score::SoftScore;
230
231    #[derive(Clone, Debug, Hash, PartialEq, Eq)]
232    struct Item { id: usize }
233
234    #[derive(Clone)]
235    struct Solution { items: Vec<Item> }
236
237    let constraint = ConstraintFactory::<Solution, SoftScore>::new()
238    .for_each(|s: &Solution| s.items.as_slice())
239    .join(equal(|i: &Item| i.id))
240    .join(equal(|i: &Item| i.id))
241    .join(equal(|i: &Item| i.id))
242    .penalize(SoftScore::of(1))
243    .named("Quadruple items");
244
245    assert_eq!(constraint.name(), "Quadruple items");
246    ```
247    */
248}