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, a: &A, b: &A, c: &A, d: &A| {
121            filter.test(s, a, b, c) && joiner.matches(a, d)
122        };
123
124        QuadConstraintStream::new_self_join_with_filter(
125            self.extractor,
126            self.key_extractor,
127            FnQuadFilter::new(combined_filter),
128        )
129    }
130}
131
132// Additional doctests for individual methods
133
134#[cfg(doctest)]
135mod doctests {
136    /* # Filter method
137
138    ```
139    use solverforge_scoring::stream::ConstraintFactory;
140    use solverforge_scoring::stream::joiner::equal;
141    use solverforge_scoring::api::constraint_set::IncrementalConstraint;
142    use solverforge_core::score::SoftScore;
143
144    #[derive(Clone, Debug, Hash, PartialEq, Eq)]
145    struct Item { group: u32, value: i32 }
146
147    #[derive(Clone)]
148    struct Solution { items: Vec<Item> }
149
150    let constraint = ConstraintFactory::<Solution, SoftScore>::new()
151    .for_each(|s: &Solution| s.items.as_slice())
152    .join(equal(|i: &Item| i.group))
153    .join(equal(|i: &Item| i.group))
154    .filter(|a: &Item, b: &Item, c: &Item| a.value + b.value + c.value > 10)
155    .penalize(SoftScore::of(1))
156    .named("High sum triples");
157
158    let solution = Solution {
159    items: vec![
160    Item { group: 1, value: 3 },
161    Item { group: 1, value: 4 },
162    Item { group: 1, value: 5 },
163    ],
164    };
165
166    // 3+4+5=12 > 10, matches
167    assert_eq!(constraint.evaluate(&solution), SoftScore::of(-1));
168    ```
169
170    # Penalize method
171
172    ```
173    use solverforge_scoring::stream::ConstraintFactory;
174    use solverforge_scoring::stream::joiner::equal;
175    use solverforge_scoring::api::constraint_set::IncrementalConstraint;
176    use solverforge_core::score::SoftScore;
177
178    #[derive(Clone, Debug, Hash, PartialEq, Eq)]
179    struct Task { priority: u32 }
180
181    #[derive(Clone)]
182    struct Solution { tasks: Vec<Task> }
183
184    let constraint = ConstraintFactory::<Solution, SoftScore>::new()
185    .for_each(|s: &Solution| s.tasks.as_slice())
186    .join(equal(|t: &Task| t.priority))
187    .join(equal(|t: &Task| t.priority))
188    .penalize(SoftScore::of(5))
189    .named("Triple priority conflict");
190
191    let solution = Solution {
192    tasks: vec![
193    Task { priority: 1 },
194    Task { priority: 1 },
195    Task { priority: 1 },
196    ],
197    };
198
199    // One triple = -5
200    assert_eq!(constraint.evaluate(&solution), SoftScore::of(-5));
201    ```
202
203    # Penalize with dynamic weight
204
205    ```
206    use solverforge_scoring::stream::ConstraintFactory;
207    use solverforge_scoring::stream::joiner::equal;
208    use solverforge_scoring::api::constraint_set::IncrementalConstraint;
209    use solverforge_core::score::SoftScore;
210
211    #[derive(Clone, Debug, Hash, PartialEq, Eq)]
212    struct Task { team: u32, cost: i64 }
213
214    #[derive(Clone)]
215    struct Solution { tasks: Vec<Task> }
216
217    let constraint = ConstraintFactory::<Solution, SoftScore>::new()
218    .for_each(|s: &Solution| s.tasks.as_slice())
219    .join(equal(|t: &Task| t.team))
220    .join(equal(|t: &Task| t.team))
221    .penalize_with(|a: &Task, b: &Task, c: &Task| {
222    SoftScore::of(a.cost + b.cost + c.cost)
223    })
224    .named("Team cost");
225
226    let solution = Solution {
227    tasks: vec![
228    Task { team: 1, cost: 2 },
229    Task { team: 1, cost: 3 },
230    Task { team: 1, cost: 5 },
231    ],
232    };
233
234    // Penalty: 2+3+5 = -10
235    assert_eq!(constraint.evaluate(&solution), SoftScore::of(-10));
236    ```
237
238    # Reward method
239
240    ```
241    use solverforge_scoring::stream::ConstraintFactory;
242    use solverforge_scoring::stream::joiner::equal;
243    use solverforge_scoring::api::constraint_set::IncrementalConstraint;
244    use solverforge_core::score::SoftScore;
245
246    #[derive(Clone, Debug, Hash, PartialEq, Eq)]
247    struct Person { team: u32 }
248
249    #[derive(Clone)]
250    struct Solution { people: Vec<Person> }
251
252    let constraint = ConstraintFactory::<Solution, SoftScore>::new()
253    .for_each(|s: &Solution| s.people.as_slice())
254    .join(equal(|p: &Person| p.team))
255    .join(equal(|p: &Person| p.team))
256    .reward(SoftScore::of(10))
257    .named("Team synergy");
258
259    let solution = Solution {
260    people: vec![
261    Person { team: 1 },
262    Person { team: 1 },
263    Person { team: 1 },
264    ],
265    };
266
267    // One triple = +10
268    assert_eq!(constraint.evaluate(&solution), SoftScore::of(10));
269    ```
270
271    # named method
272
273    ```
274    use solverforge_scoring::stream::ConstraintFactory;
275    use solverforge_scoring::stream::joiner::equal;
276    use solverforge_scoring::api::constraint_set::IncrementalConstraint;
277    use solverforge_core::score::SoftScore;
278
279    #[derive(Clone, Debug, Hash, PartialEq, Eq)]
280    struct Item { id: usize }
281
282    #[derive(Clone)]
283    struct Solution { items: Vec<Item> }
284
285    let constraint = ConstraintFactory::<Solution, SoftScore>::new()
286    .for_each(|s: &Solution| s.items.as_slice())
287    .join(equal(|i: &Item| i.id))
288    .join(equal(|i: &Item| i.id))
289    .penalize(SoftScore::of(1))
290    .named("Triple items");
291
292    assert_eq!(constraint.name(), "Triple items");
293    ```
294    */
295}