Skip to main content

solverforge_scoring/stream/
penta_stream.rs

1// Zero-erasure penta-constraint stream for five-entity constraint patterns.
2//
3// A `PentaConstraintStream` operates on quintuples of entities and supports
4// filtering, weighting, and constraint finalization. All type information
5// is preserved at compile time - no Arc, no dyn.
6//
7// # Example
8//
9// ```
10// use solverforge_scoring::stream::ConstraintFactory;
11// use solverforge_scoring::stream::joiner::equal;
12// use solverforge_scoring::api::constraint_set::IncrementalConstraint;
13// use solverforge_core::score::SoftScore;
14//
15// #[derive(Clone, Debug, Hash, PartialEq, Eq)]
16// struct Task { team: u32 }
17//
18// #[derive(Clone)]
19// struct Solution { tasks: Vec<Task> }
20//
21// // Penalize when five tasks are on the same team
22// let constraint = ConstraintFactory::<Solution, SoftScore>::new()
23//     .for_each(|s: &Solution| s.tasks.as_slice())
24//     .join_self(equal(|t: &Task| t.team))
25//     .join_self(equal(|t: &Task| t.team))
26//     .join_self(equal(|t: &Task| t.team))
27//     .join_self(equal(|t: &Task| t.team))
28//     .penalize(SoftScore::of(1))
29//     .as_constraint("Team clustering");
30//
31// let solution = Solution {
32//     tasks: vec![
33//         Task { team: 1 },
34//         Task { team: 1 },
35//         Task { team: 1 },
36//         Task { team: 1 },
37//         Task { team: 1 },
38//         Task { team: 2 },
39//     ],
40// };
41//
42// // One quintuple on team 1: (0, 1, 2, 3, 4) = -1 penalty
43// assert_eq!(constraint.evaluate(&solution), SoftScore::of(-1));
44// ```
45
46use crate::constraint::IncrementalPentaConstraint;
47
48super::arity_stream_macros::impl_arity_stream!(
49    penta,
50    PentaConstraintStream,
51    PentaConstraintBuilder,
52    IncrementalPentaConstraint
53);
54
55// Additional doctests for individual methods
56
57#[cfg(doctest)]
58mod doctests {
59    // # Filter method
60    //
61    // ```
62    // use solverforge_scoring::stream::ConstraintFactory;
63    // use solverforge_scoring::stream::joiner::equal;
64    // use solverforge_scoring::api::constraint_set::IncrementalConstraint;
65    // use solverforge_core::score::SoftScore;
66    //
67    // #[derive(Clone, Debug, Hash, PartialEq, Eq)]
68    // struct Item { group: u32, value: i32 }
69    //
70    // #[derive(Clone)]
71    // struct Solution { items: Vec<Item> }
72    //
73    // let constraint = ConstraintFactory::<Solution, SoftScore>::new()
74    //     .for_each(|s: &Solution| s.items.as_slice())
75    //     .join_self(equal(|i: &Item| i.group))
76    //     .join_self(equal(|i: &Item| i.group))
77    //     .join_self(equal(|i: &Item| i.group))
78    //     .join_self(equal(|i: &Item| i.group))
79    //     .filter(|a: &Item, b: &Item, c: &Item, d: &Item, e: &Item| {
80    //         a.value + b.value + c.value + d.value + e.value > 20
81    //     })
82    //     .penalize(SoftScore::of(1))
83    //     .as_constraint("High sum quintuples");
84    //
85    // let solution = Solution {
86    //     items: vec![
87    //         Item { group: 1, value: 3 },
88    //         Item { group: 1, value: 4 },
89    //         Item { group: 1, value: 5 },
90    //         Item { group: 1, value: 6 },
91    //         Item { group: 1, value: 7 },
92    //     ],
93    // };
94    //
95    // // 3+4+5+6+7=25 > 20, matches
96    // assert_eq!(constraint.evaluate(&solution), SoftScore::of(-1));
97    // ```
98    //
99    // # Penalize method
100    //
101    // ```
102    // use solverforge_scoring::stream::ConstraintFactory;
103    // use solverforge_scoring::stream::joiner::equal;
104    // use solverforge_scoring::api::constraint_set::IncrementalConstraint;
105    // use solverforge_core::score::SoftScore;
106    //
107    // #[derive(Clone, Debug, Hash, PartialEq, Eq)]
108    // struct Task { priority: u32 }
109    //
110    // #[derive(Clone)]
111    // struct Solution { tasks: Vec<Task> }
112    //
113    // let constraint = ConstraintFactory::<Solution, SoftScore>::new()
114    //     .for_each(|s: &Solution| s.tasks.as_slice())
115    //     .join_self(equal(|t: &Task| t.priority))
116    //     .join_self(equal(|t: &Task| t.priority))
117    //     .join_self(equal(|t: &Task| t.priority))
118    //     .join_self(equal(|t: &Task| t.priority))
119    //     .penalize(SoftScore::of(5))
120    //     .as_constraint("Quintuple priority conflict");
121    //
122    // let solution = Solution {
123    //     tasks: vec![
124    //         Task { priority: 1 },
125    //         Task { priority: 1 },
126    //         Task { priority: 1 },
127    //         Task { priority: 1 },
128    //         Task { priority: 1 },
129    //     ],
130    // };
131    //
132    // // One quintuple = -5
133    // assert_eq!(constraint.evaluate(&solution), SoftScore::of(-5));
134    // ```
135    //
136    // # Penalize with dynamic weight
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 Task { team: u32, cost: i64 }
146    //
147    // #[derive(Clone)]
148    // struct Solution { tasks: Vec<Task> }
149    //
150    // let constraint = ConstraintFactory::<Solution, SoftScore>::new()
151    //     .for_each(|s: &Solution| s.tasks.as_slice())
152    //     .join_self(equal(|t: &Task| t.team))
153    //     .join_self(equal(|t: &Task| t.team))
154    //     .join_self(equal(|t: &Task| t.team))
155    //     .join_self(equal(|t: &Task| t.team))
156    //     .penalize_with(|a: &Task, b: &Task, c: &Task, d: &Task, e: &Task| {
157    //         SoftScore::of(a.cost + b.cost + c.cost + d.cost + e.cost)
158    //     })
159    //     .as_constraint("Team cost");
160    //
161    // let solution = Solution {
162    //     tasks: vec![
163    //         Task { team: 1, cost: 1 },
164    //         Task { team: 1, cost: 2 },
165    //         Task { team: 1, cost: 3 },
166    //         Task { team: 1, cost: 4 },
167    //         Task { team: 1, cost: 5 },
168    //     ],
169    // };
170    //
171    // // Penalty: 1+2+3+4+5 = -15
172    // assert_eq!(constraint.evaluate(&solution), SoftScore::of(-15));
173    // ```
174    //
175    // # Reward method
176    //
177    // ```
178    // use solverforge_scoring::stream::ConstraintFactory;
179    // use solverforge_scoring::stream::joiner::equal;
180    // use solverforge_scoring::api::constraint_set::IncrementalConstraint;
181    // use solverforge_core::score::SoftScore;
182    //
183    // #[derive(Clone, Debug, Hash, PartialEq, Eq)]
184    // struct Person { team: u32 }
185    //
186    // #[derive(Clone)]
187    // struct Solution { people: Vec<Person> }
188    //
189    // let constraint = ConstraintFactory::<Solution, SoftScore>::new()
190    //     .for_each(|s: &Solution| s.people.as_slice())
191    //     .join_self(equal(|p: &Person| p.team))
192    //     .join_self(equal(|p: &Person| p.team))
193    //     .join_self(equal(|p: &Person| p.team))
194    //     .join_self(equal(|p: &Person| p.team))
195    //     .reward(SoftScore::of(10))
196    //     .as_constraint("Team synergy");
197    //
198    // let solution = Solution {
199    //     people: vec![
200    //         Person { team: 1 },
201    //         Person { team: 1 },
202    //         Person { team: 1 },
203    //         Person { team: 1 },
204    //         Person { team: 1 },
205    //     ],
206    // };
207    //
208    // // One quintuple = +10
209    // assert_eq!(constraint.evaluate(&solution), SoftScore::of(10));
210    // ```
211    //
212    // # as_constraint method
213    //
214    // ```
215    // use solverforge_scoring::stream::ConstraintFactory;
216    // use solverforge_scoring::stream::joiner::equal;
217    // use solverforge_scoring::api::constraint_set::IncrementalConstraint;
218    // use solverforge_core::score::SoftScore;
219    //
220    // #[derive(Clone, Debug, Hash, PartialEq, Eq)]
221    // struct Item { id: usize }
222    //
223    // #[derive(Clone)]
224    // struct Solution { items: Vec<Item> }
225    //
226    // let constraint = ConstraintFactory::<Solution, SoftScore>::new()
227    //     .for_each(|s: &Solution| s.items.as_slice())
228    //     .join_self(equal(|i: &Item| i.id))
229    //     .join_self(equal(|i: &Item| i.id))
230    //     .join_self(equal(|i: &Item| i.id))
231    //     .join_self(equal(|i: &Item| i.id))
232    //     .penalize(SoftScore::of(1))
233    //     .as_constraint("Quintuple items");
234    //
235    // assert_eq!(constraint.name(), "Quintuple items");
236    // ```
237}