Skip to main content

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::marker::PhantomData;
27
28use rayon::prelude::*;
29use solverforge_core::domain::PlanningSolution;
30use solverforge_scoring::Director;
31
32use crate::phase::Phase;
33use crate::scope::BestSolutionCallback;
34use crate::scope::SolverScope;
35
36pub use partitioner::{FunctionalPartitioner, SolutionPartitioner, ThreadCount};
37
38/// Configuration for partitioned search phase.
39#[derive(Debug, Clone)]
40pub struct PartitionedSearchConfig {
41    /// Thread count configuration.
42    pub thread_count: ThreadCount,
43    /// Whether to log partition progress.
44    pub log_progress: bool,
45}
46
47impl Default for PartitionedSearchConfig {
48    fn default() -> Self {
49        Self {
50            thread_count: ThreadCount::Auto,
51            log_progress: false,
52        }
53    }
54}
55
56/// Partitioned search phase that solves partitions in parallel.
57///
58/// This phase:
59/// 1. Partitions the solution using the provided partitioner
60/// 2. Creates a solver for each partition
61/// 3. Runs child phases on each partition in parallel
62/// 4. Merges the solved partitions back together
63///
64/// Each partition runs independently with its own solver scope.
65///
66/// # Type Parameters
67///
68/// * `S` - The solution type
69/// * `D` - The main solver's score director type
70/// * `PD` - The score director type for partition solvers
71/// * `Part` - The partitioner type (implements `SolutionPartitioner<S>`)
72/// * `SDF` - The score director factory function type
73/// * `PF` - The phase factory function type
74/// * `CP` - The child phases type (tuple of phases)
75pub struct PartitionedSearchPhase<S, D, PD, Part, SDF, PF, CP>
76where
77    S: PlanningSolution,
78    D: Director<S>,
79    PD: Director<S>,
80    Part: SolutionPartitioner<S>,
81    SDF: Fn(S) -> PD + Send + Sync,
82    PF: Fn() -> CP + Send + Sync,
83    CP: ChildPhases<S, PD>,
84{
85    /// The partitioner that splits and merges solutions.
86    partitioner: Part,
87
88    /// Factory for creating score directors for each partition.
89    score_director_factory: SDF,
90
91    /// Factory for creating child phases for each partition.
92    phase_factory: PF,
93
94    /// Configuration for this phase.
95    config: PartitionedSearchConfig,
96
97    _marker: PhantomData<fn(S, D, PD, CP)>,
98}
99
100impl<S, D, PD, Part, SDF, PF, CP> PartitionedSearchPhase<S, D, PD, Part, SDF, PF, CP>
101where
102    S: PlanningSolution,
103    D: Director<S>,
104    PD: Director<S>,
105    Part: SolutionPartitioner<S>,
106    SDF: Fn(S) -> PD + Send + Sync,
107    PF: Fn() -> CP + Send + Sync,
108    CP: ChildPhases<S, PD>,
109{
110    /// Creates a new partitioned search phase.
111    pub fn new(partitioner: Part, score_director_factory: SDF, phase_factory: PF) -> Self {
112        Self {
113            partitioner,
114            score_director_factory,
115            phase_factory,
116            config: PartitionedSearchConfig::default(),
117            _marker: PhantomData,
118        }
119    }
120
121    /// Creates a partitioned search phase with custom configuration.
122    pub fn with_config(
123        partitioner: Part,
124        score_director_factory: SDF,
125        phase_factory: PF,
126        config: PartitionedSearchConfig,
127    ) -> Self {
128        Self {
129            partitioner,
130            score_director_factory,
131            phase_factory,
132            config,
133            _marker: PhantomData,
134        }
135    }
136}
137
138impl<S, D, PD, Part, SDF, PF, CP> Debug for PartitionedSearchPhase<S, D, PD, Part, SDF, PF, CP>
139where
140    S: PlanningSolution,
141    D: Director<S>,
142    PD: Director<S>,
143    Part: SolutionPartitioner<S> + Debug,
144    SDF: Fn(S) -> PD + Send + Sync,
145    PF: Fn() -> CP + Send + Sync,
146    CP: ChildPhases<S, PD>,
147{
148    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149        f.debug_struct("PartitionedSearchPhase")
150            .field("partitioner", &self.partitioner)
151            .field("config", &self.config)
152            .finish()
153    }
154}
155
156impl<S, D, BestCb, PD, Part, SDF, PF, CP> Phase<S, D, BestCb>
157    for PartitionedSearchPhase<S, D, PD, Part, SDF, PF, CP>
158where
159    S: PlanningSolution + 'static,
160    D: Director<S>,
161    BestCb: BestSolutionCallback<S>,
162    PD: Director<S> + 'static,
163    Part: SolutionPartitioner<S>,
164    SDF: Fn(S) -> PD + Send + Sync,
165    PF: Fn() -> CP + Send + Sync,
166    CP: ChildPhases<S, PD> + Send,
167{
168    fn solve(&mut self, solver_scope: &mut SolverScope<S, D, BestCb>) {
169        // Get the current solution
170        let solution = solver_scope.score_director().working_solution().clone();
171
172        // Partition the solution
173        let partitions = self.partitioner.partition(&solution);
174        let partition_count = partitions.len();
175
176        if partition_count == 0 {
177            return;
178        }
179
180        // Determine thread count
181        let thread_count = self.config.thread_count.resolve(partition_count);
182
183        if self.config.log_progress {
184            tracing::info!(event = "phase_start", phase = "PartitionedSearch",);
185        }
186
187        // Solve partitions - parallel when multiple, sequential when single
188        let solved_partitions: Vec<S> = if thread_count == 1 || partition_count == 1 {
189            partitions
190                .into_iter()
191                .map(|p| self.solve_partition(p))
192                .collect()
193        } else {
194            partitions
195                .into_par_iter()
196                .map(|partition| {
197                    let director = (self.score_director_factory)(partition);
198                    let mut solver_scope = SolverScope::new(director);
199                    let mut phases = (self.phase_factory)();
200                    phases.solve_all(&mut solver_scope);
201                    solver_scope.take_best_or_working_solution()
202                })
203                .collect()
204        };
205
206        // Merge the solved partitions
207        let merged = self.partitioner.merge(&solution, solved_partitions);
208
209        // Update the working solution with the merged result
210        let director = solver_scope.score_director_mut();
211
212        // Replace working solution with merged result
213        let working = director.working_solution_mut();
214        *working = merged;
215
216        // Calculate the score of the merged solution
217        solver_scope.calculate_score();
218
219        // Update best solution
220        solver_scope.update_best_solution();
221
222        if self.config.log_progress {
223            if let Some(score) = solver_scope.best_score() {
224                tracing::info!(
225                    event = "phase_end",
226                    phase = "PartitionedSearch",
227                    score = %format!("{:?}", score),
228                );
229            }
230        }
231    }
232
233    fn phase_type_name(&self) -> &'static str {
234        "PartitionedSearch"
235    }
236}
237
238impl<S, D, PD, Part, SDF, PF, CP> PartitionedSearchPhase<S, D, PD, Part, SDF, PF, CP>
239where
240    S: PlanningSolution,
241    D: Director<S>,
242    PD: Director<S>,
243    Part: SolutionPartitioner<S>,
244    SDF: Fn(S) -> PD + Send + Sync,
245    PF: Fn() -> CP + Send + Sync,
246    CP: ChildPhases<S, PD>,
247{
248    /// Solves a single partition and returns the solved solution.
249    fn solve_partition(&self, partition: S) -> S {
250        // Create score director for this partition
251        let director = (self.score_director_factory)(partition);
252
253        // Create solver scope
254        let mut solver_scope = SolverScope::new(director);
255
256        // Create and run child phases
257        let mut phases = (self.phase_factory)();
258        phases.solve_all(&mut solver_scope);
259
260        // Return the best solution (or working solution if no best)
261        solver_scope.take_best_or_working_solution()
262    }
263}
264
265/// Trait for child phases that can solve a partition.
266///
267/// Implemented for tuples of phases via macro.
268pub trait ChildPhases<S, D>
269where
270    S: PlanningSolution,
271    D: Director<S>,
272{
273    /// Runs all child phases on the solver scope.
274    fn solve_all(&mut self, solver_scope: &mut SolverScope<S, D>);
275}
276
277// Implement ChildPhases for tuples using macro
278macro_rules! impl_child_phases_tuple {
279    ($($idx:tt: $P:ident),+) => {
280        impl<S, D, $($P),+> ChildPhases<S, D> for ($($P,)+)
281        where
282            S: PlanningSolution,
283            D: Director<S>,
284            $($P: Phase<S, D>,)+
285        {
286            fn solve_all(&mut self, solver_scope: &mut SolverScope<S, D>) {
287                $(
288                    self.$idx.solve(solver_scope);
289                )+
290            }
291        }
292    };
293}
294
295impl_child_phases_tuple!(0: P0);
296impl_child_phases_tuple!(0: P0, 1: P1);
297impl_child_phases_tuple!(0: P0, 1: P1, 2: P2);
298impl_child_phases_tuple!(0: P0, 1: P1, 2: P2, 3: P3);
299impl_child_phases_tuple!(0: P0, 1: P1, 2: P2, 3: P3, 4: P4);
300impl_child_phases_tuple!(0: P0, 1: P1, 2: P2, 3: P3, 4: P4, 5: P5);
301impl_child_phases_tuple!(0: P0, 1: P1, 2: P2, 3: P3, 4: P4, 5: P5, 6: P6);
302impl_child_phases_tuple!(0: P0, 1: P1, 2: P2, 3: P3, 4: P4, 5: P5, 6: P6, 7: P7);
303
304#[cfg(test)]
305mod tests {
306    use super::*;
307
308    #[test]
309    fn test_config_default() {
310        let config = PartitionedSearchConfig::default();
311        assert_eq!(config.thread_count, ThreadCount::Auto);
312        assert!(!config.log_progress);
313    }
314}