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}