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 crate::constraint::IncrementalTriConstraint;
48
49use super::collection_extract::CollectionExtract;
50use super::filter::{FnQuadFilter, QuadFilter, TriFilter};
51use super::joiner::Joiner;
52use super::quad_stream::QuadConstraintStream;
53
54super::arity_stream_macros::impl_arity_stream!(
55    tri,
56    TriConstraintStream,
57    TriConstraintBuilder,
58    IncrementalTriConstraint
59);
60
61// join method - transitions to QuadConstraintStream
62impl<S, A, K, E, KE, F, Sc> TriConstraintStream<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: CollectionExtract<S, Item = A>,
68    KE: Fn(&S, &A, usize) -> K + Send + Sync,
69    F: TriFilter<S, A, A, A>,
70    Sc: Score + 'static,
71{
72    /* Joins this stream with a fourth element to create quadruples.
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 four tasks are on the same team
89    let constraint = ConstraintFactory::<Solution, SoftScore>::new()
90    .for_each(|s: &Solution| s.tasks.as_slice())
91    .join(equal(|t: &Task| t.team))
92    .join(equal(|t: &Task| t.team))
93    .join(equal(|t: &Task| t.team))
94    .penalize(SoftScore::of(1))
95    .named("Team clustering");
96
97    let solution = Solution {
98    tasks: vec![
99    Task { team: 1 },
100    Task { team: 1 },
101    Task { team: 1 },
102    Task { team: 1 },
103    Task { team: 2 },
104    ],
105    };
106
107    // One quadruple on team 1: (0, 1, 2, 3) = -1 penalty
108    assert_eq!(constraint.evaluate(&solution), SoftScore::of(-1));
109    ```
110    */
111    pub fn join<J>(
112        self,
113        joiner: J,
114    ) -> QuadConstraintStream<S, A, K, E, KE, impl QuadFilter<S, A, A, A, A>, Sc>
115    where
116        J: Joiner<A, A> + 'static,
117        F: 'static,
118    {
119        let filter = self.filter;
120        let combined_filter = move |s: &S,
121                                    a: &A,
122                                    b: &A,
123                                    c: &A,
124                                    d: &A,
125                                    a_idx: usize,
126                                    b_idx: usize,
127                                    c_idx: usize,
128                                    _d_idx: usize| {
129            filter.test(s, a, b, c, a_idx, b_idx, c_idx) && joiner.matches(a, d)
130        };
131
132        QuadConstraintStream::new_self_join_with_filter(
133            self.extractor,
134            self.key_extractor,
135            FnQuadFilter::new(combined_filter),
136        )
137    }
138}
139
140// Additional doctests for individual methods
141
142#[cfg(doctest)]
143mod doctests {
144    /* # Filter method
145
146    ```
147    use solverforge_scoring::stream::ConstraintFactory;
148    use solverforge_scoring::stream::joiner::equal;
149    use solverforge_scoring::api::constraint_set::IncrementalConstraint;
150    use solverforge_core::score::SoftScore;
151
152    #[derive(Clone, Debug, Hash, PartialEq, Eq)]
153    struct Item { group: u32, value: i32 }
154
155    #[derive(Clone)]
156    struct Solution { items: Vec<Item> }
157
158    let constraint = ConstraintFactory::<Solution, SoftScore>::new()
159    .for_each(|s: &Solution| s.items.as_slice())
160    .join(equal(|i: &Item| i.group))
161    .join(equal(|i: &Item| i.group))
162    .filter(|a: &Item, b: &Item, c: &Item| a.value + b.value + c.value > 10)
163    .penalize(SoftScore::of(1))
164    .named("High sum triples");
165
166    let solution = Solution {
167    items: vec![
168    Item { group: 1, value: 3 },
169    Item { group: 1, value: 4 },
170    Item { group: 1, value: 5 },
171    ],
172    };
173
174    // 3+4+5=12 > 10, 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    .penalize(SoftScore::of(5))
197    .named("Triple priority conflict");
198
199    let solution = Solution {
200    tasks: vec![
201    Task { priority: 1 },
202    Task { priority: 1 },
203    Task { priority: 1 },
204    ],
205    };
206
207    // One triple = -5
208    assert_eq!(constraint.evaluate(&solution), SoftScore::of(-5));
209    ```
210
211    # Penalize with dynamic weight
212
213    ```
214    use solverforge_scoring::stream::ConstraintFactory;
215    use solverforge_scoring::stream::joiner::equal;
216    use solverforge_scoring::api::constraint_set::IncrementalConstraint;
217    use solverforge_core::score::SoftScore;
218
219    #[derive(Clone, Debug, Hash, PartialEq, Eq)]
220    struct Task { team: u32, cost: i64 }
221
222    #[derive(Clone)]
223    struct Solution { tasks: Vec<Task> }
224
225    let constraint = ConstraintFactory::<Solution, SoftScore>::new()
226    .for_each(|s: &Solution| s.tasks.as_slice())
227    .join(equal(|t: &Task| t.team))
228    .join(equal(|t: &Task| t.team))
229    .penalize(|a: &Task, b: &Task, c: &Task| {
230    SoftScore::of(a.cost + b.cost + c.cost)
231    })
232    .named("Team cost");
233
234    let solution = Solution {
235    tasks: vec![
236    Task { team: 1, cost: 2 },
237    Task { team: 1, cost: 3 },
238    Task { team: 1, cost: 5 },
239    ],
240    };
241
242    // Penalty: 2+3+5 = -10
243    assert_eq!(constraint.evaluate(&solution), SoftScore::of(-10));
244    ```
245
246    # Reward method
247
248    ```
249    use solverforge_scoring::stream::ConstraintFactory;
250    use solverforge_scoring::stream::joiner::equal;
251    use solverforge_scoring::api::constraint_set::IncrementalConstraint;
252    use solverforge_core::score::SoftScore;
253
254    #[derive(Clone, Debug, Hash, PartialEq, Eq)]
255    struct Person { team: u32 }
256
257    #[derive(Clone)]
258    struct Solution { people: Vec<Person> }
259
260    let constraint = ConstraintFactory::<Solution, SoftScore>::new()
261    .for_each(|s: &Solution| s.people.as_slice())
262    .join(equal(|p: &Person| p.team))
263    .join(equal(|p: &Person| p.team))
264    .reward(SoftScore::of(10))
265    .named("Team synergy");
266
267    let solution = Solution {
268    people: vec![
269    Person { team: 1 },
270    Person { team: 1 },
271    Person { team: 1 },
272    ],
273    };
274
275    // One triple = +10
276    assert_eq!(constraint.evaluate(&solution), SoftScore::of(10));
277    ```
278
279    # named method
280
281    ```
282    use solverforge_scoring::stream::ConstraintFactory;
283    use solverforge_scoring::stream::joiner::equal;
284    use solverforge_scoring::api::constraint_set::IncrementalConstraint;
285    use solverforge_core::score::SoftScore;
286
287    #[derive(Clone, Debug, Hash, PartialEq, Eq)]
288    struct Item { id: usize }
289
290    #[derive(Clone)]
291    struct Solution { items: Vec<Item> }
292
293    let constraint = ConstraintFactory::<Solution, SoftScore>::new()
294    .for_each(|s: &Solution| s.items.as_slice())
295    .join(equal(|i: &Item| i.id))
296    .join(equal(|i: &Item| i.id))
297    .penalize(SoftScore::of(1))
298    .named("Triple items");
299
300    assert_eq!(constraint.name(), "Triple items");
301    ```
302    */
303}