solverforge_solver/phase/partitioned/
mod.rs

1//! Partitioned search phase for parallel solving.
2//!
3//! Partitioned search splits a large problem into independent sub-problems
4//! (partitions) that can be solved in parallel, then merges the results.
5//!
6//! # Usage
7//!
8//! 1. Define a partitioner that knows how to split and merge your solution type
9//! 2. Create a partitioned search phase with child phases
10//! 3. The phase will partition the solution, solve each partition, and merge
11//!
12//! # Example
13//!
14//! ```
15//! use solverforge_solver::phase::partitioned::{PartitionedSearchConfig, ThreadCount};
16//!
17//! let config = PartitionedSearchConfig {
18//!     thread_count: ThreadCount::Specific(4),
19//!     log_progress: true,
20//! };
21//! ```
22
23mod partitioner;
24
25use std::fmt::Debug;
26use std::sync::{Arc, Mutex};
27use std::thread;
28
29use solverforge_core::domain::PlanningSolution;
30use solverforge_scoring::ScoreDirector;
31
32use crate::phase::Phase;
33use crate::scope::SolverScope;
34
35pub use partitioner::{FunctionalPartitioner, SolutionPartitioner, ThreadCount};
36
37/// Configuration for partitioned search phase.
38#[derive(Debug, Clone)]
39pub struct PartitionedSearchConfig {
40    /// Thread count configuration.
41    pub thread_count: ThreadCount,
42    /// Whether to log partition progress.
43    pub log_progress: bool,
44}
45
46impl Default for PartitionedSearchConfig {
47    fn default() -> Self {
48        Self {
49            thread_count: ThreadCount::Auto,
50            log_progress: false,
51        }
52    }
53}
54
55/// A factory function for creating score directors.
56pub type ScoreDirectorFactory<S> = Arc<dyn Fn(S) -> Box<dyn ScoreDirector<S>> + Send + Sync>;
57
58/// A factory function for creating phases.
59pub type PhaseFactory<S> = Arc<dyn Fn() -> Vec<Box<dyn Phase<S>>> + Send + Sync>;
60
61/// Partitioned search phase that solves partitions in parallel.
62///
63/// This phase:
64/// 1. Partitions the solution using the provided partitioner
65/// 2. Creates a solver for each partition
66/// 3. Runs child phases on each partition in parallel
67/// 4. Merges the solved partitions back together
68///
69/// Each partition runs independently with its own solver scope.
70pub struct PartitionedSearchPhase<S: PlanningSolution> {
71    /// The partitioner that splits and merges solutions.
72    partitioner: Box<dyn SolutionPartitioner<S>>,
73
74    /// Factory for creating score directors for each partition.
75    score_director_factory: ScoreDirectorFactory<S>,
76
77    /// Factory for creating child phases for each partition.
78    phase_factory: PhaseFactory<S>,
79
80    /// Configuration for this phase.
81    config: PartitionedSearchConfig,
82}
83
84impl<S: PlanningSolution> PartitionedSearchPhase<S> {
85    /// Creates a new partitioned search phase.
86    pub fn new(
87        partitioner: Box<dyn SolutionPartitioner<S>>,
88        score_director_factory: ScoreDirectorFactory<S>,
89        phase_factory: PhaseFactory<S>,
90    ) -> Self {
91        Self {
92            partitioner,
93            score_director_factory,
94            phase_factory,
95            config: PartitionedSearchConfig::default(),
96        }
97    }
98
99    /// Creates a partitioned search phase with custom configuration.
100    pub fn with_config(
101        partitioner: Box<dyn SolutionPartitioner<S>>,
102        score_director_factory: ScoreDirectorFactory<S>,
103        phase_factory: PhaseFactory<S>,
104        config: PartitionedSearchConfig,
105    ) -> Self {
106        Self {
107            partitioner,
108            score_director_factory,
109            phase_factory,
110            config,
111        }
112    }
113
114    /// Solves a single partition and returns the solved solution.
115    fn solve_partition(
116        partition: S,
117        score_director_factory: &ScoreDirectorFactory<S>,
118        phase_factory: &PhaseFactory<S>,
119    ) -> S {
120        // Create score director for this partition
121        let director = (score_director_factory)(partition);
122
123        // Create solver scope
124        let mut solver_scope = SolverScope::new(director);
125
126        // Create and run child phases
127        let mut phases = (phase_factory)();
128        for phase in phases.iter_mut() {
129            phase.solve(&mut solver_scope);
130        }
131
132        // Return the best solution (or working solution if no best)
133        solver_scope.take_best_or_working_solution()
134    }
135}
136
137impl<S: PlanningSolution> Debug for PartitionedSearchPhase<S> {
138    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
139        f.debug_struct("PartitionedSearchPhase")
140            .field("partitioner", &self.partitioner)
141            .field("config", &self.config)
142            .finish()
143    }
144}
145
146impl<S: PlanningSolution + 'static> Phase<S> for PartitionedSearchPhase<S> {
147    fn solve(&mut self, solver_scope: &mut SolverScope<S>) {
148        // Get the current solution
149        let solution = solver_scope.score_director().working_solution().clone();
150
151        // Partition the solution
152        let partitions = self.partitioner.partition(&solution);
153        let partition_count = partitions.len();
154
155        if partition_count == 0 {
156            return;
157        }
158
159        // Determine thread count
160        let thread_count = self.config.thread_count.resolve(partition_count);
161
162        if self.config.log_progress {
163            println!(
164                "[PartitionedSearch] Solving {} partitions with {} threads",
165                partition_count, thread_count
166            );
167        }
168
169        // Clone factories for threads
170        let score_director_factory = Arc::clone(&self.score_director_factory);
171        let phase_factory = Arc::clone(&self.phase_factory);
172
173        // Solve partitions
174        let solved_partitions: Vec<S> = if thread_count == 1 || partition_count == 1 {
175            // Sequential execution
176            partitions
177                .into_iter()
178                .map(|p| Self::solve_partition(p, &score_director_factory, &phase_factory))
179                .collect()
180        } else {
181            // Parallel execution
182            let results: Arc<Mutex<Vec<Option<S>>>> =
183                Arc::new(Mutex::new(vec![None; partition_count]));
184
185            thread::scope(|s| {
186                for (i, partition) in partitions.into_iter().enumerate() {
187                    let results = Arc::clone(&results);
188                    let sdf = Arc::clone(&score_director_factory);
189                    let pf = Arc::clone(&phase_factory);
190
191                    s.spawn(move || {
192                        let solved = Self::solve_partition(partition, &sdf, &pf);
193                        let mut r = results.lock().unwrap();
194                        r[i] = Some(solved);
195                    });
196                }
197            });
198
199            // Extract results
200            let results = Arc::try_unwrap(results)
201                .unwrap_or_else(|_| panic!("All threads should be done"))
202                .into_inner()
203                .unwrap();
204
205            results.into_iter().map(|opt| opt.unwrap()).collect()
206        };
207
208        // Merge the solved partitions
209        let merged = self.partitioner.merge(&solution, solved_partitions);
210
211        // Update the working solution with the merged result
212        // We need to calculate the score and update best solution
213        let director = solver_scope.score_director_mut();
214
215        // Replace working solution with merged result
216        let working = director.working_solution_mut();
217        *working = merged;
218
219        // Calculate the score of the merged solution
220        solver_scope.calculate_score();
221
222        // Update best solution
223        solver_scope.update_best_solution();
224
225        if self.config.log_progress {
226            if let Some(score) = solver_scope.best_score() {
227                println!(
228                    "[PartitionedSearch] Completed with merged score: {:?}",
229                    score
230                );
231            }
232        }
233    }
234
235    fn phase_type_name(&self) -> &'static str {
236        "PartitionedSearch"
237    }
238}
239
240#[cfg(test)]
241mod tests {
242    use super::*;
243    use solverforge_core::score::SimpleScore;
244
245    #[derive(Clone, Debug)]
246    struct TestSolution {
247        values: Vec<i32>,
248        score: Option<SimpleScore>,
249    }
250
251    impl PlanningSolution for TestSolution {
252        type Score = SimpleScore;
253
254        fn score(&self) -> Option<Self::Score> {
255            self.score
256        }
257
258        fn set_score(&mut self, score: Option<Self::Score>) {
259            self.score = score;
260        }
261    }
262
263    #[test]
264    fn test_config_default() {
265        let config = PartitionedSearchConfig::default();
266        assert_eq!(config.thread_count, ThreadCount::Auto);
267        assert!(!config.log_progress);
268    }
269
270    #[test]
271    fn test_phase_type_name() {
272        let test_solution = TestSolution {
273            values: vec![1, 2, 3],
274            score: None,
275        };
276        assert_eq!(test_solution.values.len(), 3);
277
278        let partitioner = Box::new(FunctionalPartitioner::new(
279            |s: &TestSolution| vec![s.clone()],
280            |_, partitions| partitions.into_iter().next().unwrap(),
281        ));
282
283        let sdf: ScoreDirectorFactory<TestSolution> = Arc::new(|_s| {
284            panic!("Should not be called in this test");
285        });
286
287        let pf: PhaseFactory<TestSolution> = Arc::new(Vec::new);
288
289        let phase = PartitionedSearchPhase::new(partitioner, sdf, pf);
290        assert_eq!(phase.phase_type_name(), "PartitionedSearch");
291    }
292
293    #[test]
294    fn test_phase_debug() {
295        let partitioner = Box::new(FunctionalPartitioner::new(
296            |s: &TestSolution| vec![s.clone()],
297            |_, partitions| partitions.into_iter().next().unwrap(),
298        ));
299
300        let sdf: ScoreDirectorFactory<TestSolution> = Arc::new(|_s| {
301            panic!("Should not be called in this test");
302        });
303
304        let pf: PhaseFactory<TestSolution> = Arc::new(Vec::new);
305
306        let phase = PartitionedSearchPhase::new(partitioner, sdf, pf);
307        let debug = format!("{:?}", phase);
308
309        assert!(debug.contains("PartitionedSearchPhase"));
310        assert!(debug.contains("FunctionalPartitioner"));
311    }
312}