Skip to main content

solverforge_scoring/stream/
tri_stream.rs

1/* Zero-erasure tri-constraint stream for three-entity constraint patterns.
2
3A `TriConstraintStream` operates on triples 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 three 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.penalize(SoftScore::of(1))
27.named("Team clustering");
28
29let solution = Solution {
30tasks: vec![
31Task { team: 1 },
32Task { team: 1 },
33Task { team: 1 },
34Task { team: 2 },
35],
36};
37
38// One triple on team 1: (0, 1, 2) = -1 penalty
39assert_eq!(constraint.evaluate(&solution), SoftScore::of(-1));
40```
41*/
42
43use std::hash::Hash;
44
45use solverforge_core::score::Score;
46
47use super::collection_extract::CollectionExtract;
48use super::filter::{FnQuadFilter, QuadFilter, TriFilter};
49use super::joiner::Joiner;
50use super::quad_stream::QuadConstraintStream;
51
52super::arity_stream_macros::impl_arity_stream!(tri, TriConstraintStream, TriConstraintBuilder);
53
54// join method - transitions to QuadConstraintStream
55impl<S, A, K, E, KE, F, Sc> TriConstraintStream<S, A, K, E, KE, F, Sc>
56where
57    S: Send + Sync + 'static,
58    A: Clone + Hash + PartialEq + Send + Sync + 'static,
59    K: Eq + Hash + Clone + Send + Sync,
60    E: CollectionExtract<S, Item = A>,
61    KE: Fn(&S, &A, usize) -> K + Send + Sync,
62    F: TriFilter<S, A, A, A>,
63    Sc: Score + 'static,
64{
65    /* Joins this stream with a fourth element to create quadruples.
66
67    # Example
68
69    ```
70    use solverforge_scoring::stream::ConstraintFactory;
71    use solverforge_scoring::stream::joiner::equal;
72    use solverforge_scoring::api::constraint_set::IncrementalConstraint;
73    use solverforge_core::score::SoftScore;
74
75    #[derive(Clone, Debug, Hash, PartialEq, Eq)]
76    struct Task { team: u32 }
77
78    #[derive(Clone)]
79    struct Solution { tasks: Vec<Task> }
80
81    // Penalize when four tasks are on the same team
82    let constraint = ConstraintFactory::<Solution, SoftScore>::new()
83    .for_each(|s: &Solution| s.tasks.as_slice())
84    .join(equal(|t: &Task| t.team))
85    .join(equal(|t: &Task| t.team))
86    .join(equal(|t: &Task| t.team))
87    .penalize(SoftScore::of(1))
88    .named("Team clustering");
89
90    let solution = Solution {
91    tasks: vec![
92    Task { team: 1 },
93    Task { team: 1 },
94    Task { team: 1 },
95    Task { team: 1 },
96    Task { team: 2 },
97    ],
98    };
99
100    // One quadruple on team 1: (0, 1, 2, 3) = -1 penalty
101    assert_eq!(constraint.evaluate(&solution), SoftScore::of(-1));
102    ```
103    */
104    pub fn join<J>(
105        self,
106        joiner: J,
107    ) -> QuadConstraintStream<S, A, K, E, KE, impl QuadFilter<S, A, A, A, A>, Sc>
108    where
109        J: Joiner<A, A> + 'static,
110        F: 'static,
111    {
112        let filter = self.filter;
113        let combined_filter = move |s: &S,
114                                    a: &A,
115                                    b: &A,
116                                    c: &A,
117                                    d: &A,
118                                    a_idx: usize,
119                                    b_idx: usize,
120                                    c_idx: usize,
121                                    _d_idx: usize| {
122            filter.test(s, a, b, c, a_idx, b_idx, c_idx) && joiner.matches(a, d)
123        };
124
125        QuadConstraintStream::new_self_join_with_filter(
126            self.extractor,
127            self.key_extractor,
128            FnQuadFilter::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(equal(|i: &Item| i.group))
154    .join(equal(|i: &Item| i.group))
155    .filter(|a: &Item, b: &Item, c: &Item| a.value + b.value + c.value > 10)
156    .penalize(SoftScore::of(1))
157    .named("High sum triples");
158
159    let solution = Solution {
160    items: vec![
161    Item { group: 1, value: 3 },
162    Item { group: 1, value: 4 },
163    Item { group: 1, value: 5 },
164    ],
165    };
166
167    // 3+4+5=12 > 10, matches
168    assert_eq!(constraint.evaluate(&solution), SoftScore::of(-1));
169    ```
170
171    # Penalize method
172
173    ```
174    use solverforge_scoring::stream::ConstraintFactory;
175    use solverforge_scoring::stream::joiner::equal;
176    use solverforge_scoring::api::constraint_set::IncrementalConstraint;
177    use solverforge_core::score::SoftScore;
178
179    #[derive(Clone, Debug, Hash, PartialEq, Eq)]
180    struct Task { priority: u32 }
181
182    #[derive(Clone)]
183    struct Solution { tasks: Vec<Task> }
184
185    let constraint = ConstraintFactory::<Solution, SoftScore>::new()
186    .for_each(|s: &Solution| s.tasks.as_slice())
187    .join(equal(|t: &Task| t.priority))
188    .join(equal(|t: &Task| t.priority))
189    .penalize(SoftScore::of(5))
190    .named("Triple priority conflict");
191
192    let solution = Solution {
193    tasks: vec![
194    Task { priority: 1 },
195    Task { priority: 1 },
196    Task { priority: 1 },
197    ],
198    };
199
200    // One triple = -5
201    assert_eq!(constraint.evaluate(&solution), SoftScore::of(-5));
202    ```
203
204    # Penalize with dynamic weight
205
206    ```
207    use solverforge_scoring::stream::ConstraintFactory;
208    use solverforge_scoring::stream::joiner::equal;
209    use solverforge_scoring::api::constraint_set::IncrementalConstraint;
210    use solverforge_core::score::SoftScore;
211
212    #[derive(Clone, Debug, Hash, PartialEq, Eq)]
213    struct Task { team: u32, cost: i64 }
214
215    #[derive(Clone)]
216    struct Solution { tasks: Vec<Task> }
217
218    let constraint = ConstraintFactory::<Solution, SoftScore>::new()
219    .for_each(|s: &Solution| s.tasks.as_slice())
220    .join(equal(|t: &Task| t.team))
221    .join(equal(|t: &Task| t.team))
222    .penalize(|a: &Task, b: &Task, c: &Task| {
223    SoftScore::of(a.cost + b.cost + c.cost)
224    })
225    .named("Team cost");
226
227    let solution = Solution {
228    tasks: vec![
229    Task { team: 1, cost: 2 },
230    Task { team: 1, cost: 3 },
231    Task { team: 1, cost: 5 },
232    ],
233    };
234
235    // Penalty: 2+3+5 = -10
236    assert_eq!(constraint.evaluate(&solution), SoftScore::of(-10));
237    ```
238
239    # Reward method
240
241    ```
242    use solverforge_scoring::stream::ConstraintFactory;
243    use solverforge_scoring::stream::joiner::equal;
244    use solverforge_scoring::api::constraint_set::IncrementalConstraint;
245    use solverforge_core::score::SoftScore;
246
247    #[derive(Clone, Debug, Hash, PartialEq, Eq)]
248    struct Person { team: u32 }
249
250    #[derive(Clone)]
251    struct Solution { people: Vec<Person> }
252
253    let constraint = ConstraintFactory::<Solution, SoftScore>::new()
254    .for_each(|s: &Solution| s.people.as_slice())
255    .join(equal(|p: &Person| p.team))
256    .join(equal(|p: &Person| p.team))
257    .reward(SoftScore::of(10))
258    .named("Team synergy");
259
260    let solution = Solution {
261    people: vec![
262    Person { team: 1 },
263    Person { team: 1 },
264    Person { team: 1 },
265    ],
266    };
267
268    // One triple = +10
269    assert_eq!(constraint.evaluate(&solution), SoftScore::of(10));
270    ```
271
272    # named method
273
274    ```
275    use solverforge_scoring::stream::ConstraintFactory;
276    use solverforge_scoring::stream::joiner::equal;
277    use solverforge_scoring::api::constraint_set::IncrementalConstraint;
278    use solverforge_core::score::SoftScore;
279
280    #[derive(Clone, Debug, Hash, PartialEq, Eq)]
281    struct Item { id: usize }
282
283    #[derive(Clone)]
284    struct Solution { items: Vec<Item> }
285
286    let constraint = ConstraintFactory::<Solution, SoftScore>::new()
287    .for_each(|s: &Solution| s.items.as_slice())
288    .join(equal(|i: &Item| i.id))
289    .join(equal(|i: &Item| i.id))
290    .penalize(SoftScore::of(1))
291    .named("Triple items");
292
293    assert_eq!(constraint.name(), "Triple items");
294    ```
295    */
296}