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