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, a: &A, b: &A, c: &A, d: &A, e: &A| {
125            filter.test(s, a, b, c, d) && joiner.matches(a, e)
126        };
127
128        PentaConstraintStream::new_self_join_with_filter(
129            self.extractor,
130            self.key_extractor,
131            FnPentaFilter::new(combined_filter),
132        )
133    }
134}
135
136// Additional doctests for individual methods
137
138#[cfg(doctest)]
139mod doctests {
140    /* # Filter method
141
142    ```
143    use solverforge_scoring::stream::ConstraintFactory;
144    use solverforge_scoring::stream::joiner::equal;
145    use solverforge_scoring::api::constraint_set::IncrementalConstraint;
146    use solverforge_core::score::SoftScore;
147
148    #[derive(Clone, Debug, Hash, PartialEq, Eq)]
149    struct Item { group: u32, value: i32 }
150
151    #[derive(Clone)]
152    struct Solution { items: Vec<Item> }
153
154    let constraint = ConstraintFactory::<Solution, SoftScore>::new()
155    .for_each(|s: &Solution| s.items.as_slice())
156    .join(equal(|i: &Item| i.group))
157    .join(equal(|i: &Item| i.group))
158    .join(equal(|i: &Item| i.group))
159    .filter(|a: &Item, b: &Item, c: &Item, d: &Item| {
160    a.value + b.value + c.value + d.value > 15
161    })
162    .penalize(SoftScore::of(1))
163    .named("High sum quadruples");
164
165    let solution = Solution {
166    items: vec![
167    Item { group: 1, value: 3 },
168    Item { group: 1, value: 4 },
169    Item { group: 1, value: 5 },
170    Item { group: 1, value: 6 },
171    ],
172    };
173
174    // 3+4+5+6=18 > 15, matches
175    assert_eq!(constraint.evaluate(&solution), SoftScore::of(-1));
176    ```
177
178    # Penalize method
179
180    ```
181    use solverforge_scoring::stream::ConstraintFactory;
182    use solverforge_scoring::stream::joiner::equal;
183    use solverforge_scoring::api::constraint_set::IncrementalConstraint;
184    use solverforge_core::score::SoftScore;
185
186    #[derive(Clone, Debug, Hash, PartialEq, Eq)]
187    struct Task { priority: u32 }
188
189    #[derive(Clone)]
190    struct Solution { tasks: Vec<Task> }
191
192    let constraint = ConstraintFactory::<Solution, SoftScore>::new()
193    .for_each(|s: &Solution| s.tasks.as_slice())
194    .join(equal(|t: &Task| t.priority))
195    .join(equal(|t: &Task| t.priority))
196    .join(equal(|t: &Task| t.priority))
197    .penalize(SoftScore::of(5))
198    .named("Quadruple priority conflict");
199
200    let solution = Solution {
201    tasks: vec![
202    Task { priority: 1 },
203    Task { priority: 1 },
204    Task { priority: 1 },
205    Task { priority: 1 },
206    ],
207    };
208
209    // One quadruple = -5
210    assert_eq!(constraint.evaluate(&solution), SoftScore::of(-5));
211    ```
212
213    # named method
214
215    ```
216    use solverforge_scoring::stream::ConstraintFactory;
217    use solverforge_scoring::stream::joiner::equal;
218    use solverforge_scoring::api::constraint_set::IncrementalConstraint;
219    use solverforge_core::score::SoftScore;
220
221    #[derive(Clone, Debug, Hash, PartialEq, Eq)]
222    struct Item { id: usize }
223
224    #[derive(Clone)]
225    struct Solution { items: Vec<Item> }
226
227    let constraint = ConstraintFactory::<Solution, SoftScore>::new()
228    .for_each(|s: &Solution| s.items.as_slice())
229    .join(equal(|i: &Item| i.id))
230    .join(equal(|i: &Item| i.id))
231    .join(equal(|i: &Item| i.id))
232    .penalize(SoftScore::of(1))
233    .named("Quadruple items");
234
235    assert_eq!(constraint.name(), "Quadruple items");
236    ```
237    */
238}