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::SimpleScore;
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, SimpleScore>::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(SimpleScore::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), SimpleScore::of(-1));
44//! ```
45
46use crate::constraint::penta_incremental::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::SimpleScore;
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, SimpleScore>::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(SimpleScore::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), SimpleScore::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::SimpleScore;
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, SimpleScore>::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(SimpleScore::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), SimpleScore::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::SimpleScore;
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, SimpleScore>::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 //! SimpleScore::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), SimpleScore::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::SimpleScore;
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, SimpleScore>::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(SimpleScore::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), SimpleScore::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::SimpleScore;
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, SimpleScore>::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(SimpleScore::of(1))
233 //! .as_constraint("Quintuple items");
234 //!
235 //! assert_eq!(constraint.name(), "Quintuple items");
236 //! ```
237}