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::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}