solverforge_solver/phase/partitioned/
partitioner.rs

1//! Solution partitioner for dividing problems into independent sub-problems.
2//!
3//! Partitioners split a large problem into smaller pieces that can be
4//! solved independently (potentially in parallel), then merged back together.
5
6use std::fmt::Debug;
7
8use solverforge_core::domain::PlanningSolution;
9
10/// Splits a solution into independent partitions for parallel solving.
11///
12/// Each partition should be solvable independently without affecting
13/// the correctness of other partitions. The partitioner must also be
14/// able to merge the solved partitions back into a complete solution.
15///
16/// # Type Parameters
17///
18/// - `S`: The planning solution type
19///
20/// # Example
21///
22/// For a school timetabling problem, a natural partitioning might be
23/// by room or by time period, where each partition contains lessons
24/// that don't interact with lessons in other partitions.
25pub trait SolutionPartitioner<S: PlanningSolution>: Send + Sync + Debug {
26    /// Splits the solution into independent partitions.
27    ///
28    /// Each returned solution should be a subset of the original that
29    /// can be optimized independently. The union of all partitions should
30    /// cover all entities in the original solution.
31    ///
32    /// # Arguments
33    ///
34    /// * `solution` - The solution to partition
35    ///
36    /// # Returns
37    ///
38    /// A vector of partial solutions, one per partition.
39    fn partition(&self, solution: &S) -> Vec<S>;
40
41    /// Merges solved partitions back into a complete solution.
42    ///
43    /// This is called after all partitions have been solved to combine
44    /// them into the final result.
45    ///
46    /// # Arguments
47    ///
48    /// * `original` - The original unpartitioned solution
49    /// * `partitions` - The solved partition solutions
50    ///
51    /// # Returns
52    ///
53    /// The merged complete solution.
54    fn merge(&self, original: &S, partitions: Vec<S>) -> S;
55
56    /// Returns the recommended number of partitions.
57    ///
58    /// This can be used by the partitioned search phase to determine
59    /// how many threads to use. Returns `None` if no recommendation.
60    fn recommended_partition_count(&self) -> Option<usize> {
61        None
62    }
63}
64
65/// A simple partitioner that creates a specified number of partitions.
66///
67/// This is a reference implementation that can be customized via
68/// closures for the actual partitioning and merging logic.
69pub struct FunctionalPartitioner<S, PF, MF>
70where
71    S: PlanningSolution,
72    PF: Fn(&S) -> Vec<S> + Send + Sync,
73    MF: Fn(&S, Vec<S>) -> S + Send + Sync,
74{
75    partition_fn: PF,
76    merge_fn: MF,
77    recommended_count: Option<usize>,
78    _phantom: std::marker::PhantomData<S>,
79}
80
81impl<S, PF, MF> FunctionalPartitioner<S, PF, MF>
82where
83    S: PlanningSolution,
84    PF: Fn(&S) -> Vec<S> + Send + Sync,
85    MF: Fn(&S, Vec<S>) -> S + Send + Sync,
86{
87    /// Creates a new functional partitioner.
88    pub fn new(partition_fn: PF, merge_fn: MF) -> Self {
89        Self {
90            partition_fn,
91            merge_fn,
92            recommended_count: None,
93            _phantom: std::marker::PhantomData,
94        }
95    }
96
97    /// Sets the recommended partition count.
98    pub fn with_recommended_count(mut self, count: usize) -> Self {
99        self.recommended_count = Some(count);
100        self
101    }
102}
103
104impl<S, PF, MF> Debug for FunctionalPartitioner<S, PF, MF>
105where
106    S: PlanningSolution,
107    PF: Fn(&S) -> Vec<S> + Send + Sync,
108    MF: Fn(&S, Vec<S>) -> S + Send + Sync,
109{
110    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111        f.debug_struct("FunctionalPartitioner")
112            .field("recommended_count", &self.recommended_count)
113            .finish()
114    }
115}
116
117impl<S, PF, MF> SolutionPartitioner<S> for FunctionalPartitioner<S, PF, MF>
118where
119    S: PlanningSolution,
120    PF: Fn(&S) -> Vec<S> + Send + Sync,
121    MF: Fn(&S, Vec<S>) -> S + Send + Sync,
122{
123    fn partition(&self, solution: &S) -> Vec<S> {
124        (self.partition_fn)(solution)
125    }
126
127    fn merge(&self, original: &S, partitions: Vec<S>) -> S {
128        (self.merge_fn)(original, partitions)
129    }
130
131    fn recommended_partition_count(&self) -> Option<usize> {
132        self.recommended_count
133    }
134}
135
136/// Thread count configuration for partitioned search.
137#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
138pub enum ThreadCount {
139    /// Automatically determine based on available CPU cores.
140    #[default]
141    Auto,
142    /// Use all available CPU cores.
143    Unlimited,
144    /// Use a specific number of threads.
145    Specific(usize),
146}
147
148impl ThreadCount {
149    /// Resolves the thread count to an actual number.
150    ///
151    /// # Arguments
152    ///
153    /// * `partition_count` - Number of partitions to process
154    ///
155    /// # Returns
156    ///
157    /// The number of threads to use.
158    pub fn resolve(&self, partition_count: usize) -> usize {
159        match self {
160            ThreadCount::Auto => {
161                let cpus = std::thread::available_parallelism()
162                    .map(|p| p.get())
163                    .unwrap_or(1);
164                std::cmp::min(cpus, partition_count)
165            }
166            ThreadCount::Unlimited => std::thread::available_parallelism()
167                .map(|p| p.get())
168                .unwrap_or(1),
169            ThreadCount::Specific(n) => std::cmp::min(*n, partition_count),
170        }
171    }
172}
173
174impl std::fmt::Display for ThreadCount {
175    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
176        match self {
177            ThreadCount::Auto => write!(f, "Auto"),
178            ThreadCount::Unlimited => write!(f, "Unlimited"),
179            ThreadCount::Specific(n) => write!(f, "{}", n),
180        }
181    }
182}
183
184#[cfg(test)]
185mod tests {
186    use super::*;
187    use solverforge_core::score::SimpleScore;
188
189    #[derive(Clone, Debug)]
190    struct TestSolution {
191        values: Vec<i32>,
192        score: Option<SimpleScore>,
193    }
194
195    impl PlanningSolution for TestSolution {
196        type Score = SimpleScore;
197
198        fn score(&self) -> Option<Self::Score> {
199            self.score
200        }
201
202        fn set_score(&mut self, score: Option<Self::Score>) {
203            self.score = score;
204        }
205    }
206
207    #[test]
208    fn test_thread_count_default() {
209        assert_eq!(ThreadCount::default(), ThreadCount::Auto);
210    }
211
212    #[test]
213    fn test_thread_count_display() {
214        assert_eq!(format!("{}", ThreadCount::Auto), "Auto");
215        assert_eq!(format!("{}", ThreadCount::Unlimited), "Unlimited");
216        assert_eq!(format!("{}", ThreadCount::Specific(4)), "4");
217    }
218
219    #[test]
220    fn test_thread_count_resolve_specific() {
221        assert_eq!(ThreadCount::Specific(4).resolve(10), 4);
222        assert_eq!(ThreadCount::Specific(10).resolve(4), 4); // Capped to partition count
223    }
224
225    #[test]
226    fn test_thread_count_resolve_auto() {
227        let count = ThreadCount::Auto.resolve(100);
228        assert!(count > 0);
229    }
230
231    #[test]
232    fn test_functional_partitioner() {
233        let partitioner = FunctionalPartitioner::new(
234            |s: &TestSolution| {
235                // Split into two partitions
236                let mid = s.values.len() / 2;
237                vec![
238                    TestSolution {
239                        values: s.values[..mid].to_vec(),
240                        score: None,
241                    },
242                    TestSolution {
243                        values: s.values[mid..].to_vec(),
244                        score: None,
245                    },
246                ]
247            },
248            |_original, partitions| {
249                // Merge partitions
250                let mut values = Vec::new();
251                for p in partitions {
252                    values.extend(p.values);
253                }
254                TestSolution {
255                    values,
256                    score: None,
257                }
258            },
259        );
260
261        let solution = TestSolution {
262            values: vec![1, 2, 3, 4],
263            score: None,
264        };
265
266        let partitions = partitioner.partition(&solution);
267        assert_eq!(partitions.len(), 2);
268        assert_eq!(partitions[0].values, vec![1, 2]);
269        assert_eq!(partitions[1].values, vec![3, 4]);
270
271        let merged = partitioner.merge(&solution, partitions);
272        assert_eq!(merged.values, vec![1, 2, 3, 4]);
273    }
274
275    #[test]
276    fn test_partitioner_debug() {
277        let partitioner = FunctionalPartitioner::new(
278            |_: &TestSolution| Vec::new(),
279            |original: &TestSolution, _| original.clone(),
280        )
281        .with_recommended_count(4);
282
283        let debug = format!("{:?}", partitioner);
284        assert!(debug.contains("FunctionalPartitioner"));
285        assert!(debug.contains("recommended_count"));
286    }
287}