Skip to main content

solverforge_solver/phase/partitioned/
phase.rs

1//! PartitionedSearchPhase implementation.
2
3use std::fmt::Debug;
4use std::marker::PhantomData;
5
6use rayon::prelude::*;
7use solverforge_core::domain::PlanningSolution;
8use solverforge_scoring::Director;
9
10use crate::phase::Phase;
11use crate::scope::BestSolutionCallback;
12use crate::scope::SolverScope;
13
14use super::child_phases::ChildPhases;
15use super::config::PartitionedSearchConfig;
16use super::partitioner::SolutionPartitioner;
17
18/// Partitioned search phase that solves partitions in parallel.
19///
20/// This phase:
21/// 1. Partitions the solution using the provided partitioner
22/// 2. Creates a solver for each partition
23/// 3. Runs child phases on each partition in parallel
24/// 4. Merges the solved partitions back together
25///
26/// Each partition runs independently with its own solver scope.
27///
28/// # Type Parameters
29///
30/// * `S` - The solution type
31/// * `D` - The main solver's score director type
32/// * `PD` - The score director type for partition solvers
33/// * `Part` - The partitioner type (implements `SolutionPartitioner<S>`)
34/// * `SDF` - The score director factory function type
35/// * `PF` - The phase factory function type
36/// * `CP` - The child phases type (tuple of phases)
37pub struct PartitionedSearchPhase<S, D, PD, Part, SDF, PF, CP>
38where
39    S: PlanningSolution,
40    D: Director<S>,
41    PD: Director<S>,
42    Part: SolutionPartitioner<S>,
43    SDF: Fn(S) -> PD + Send + Sync,
44    PF: Fn() -> CP + Send + Sync,
45    CP: ChildPhases<S, PD>,
46{
47    /// The partitioner that splits and merges solutions.
48    partitioner: Part,
49
50    /// Factory for creating score directors for each partition.
51    score_director_factory: SDF,
52
53    /// Factory for creating child phases for each partition.
54    phase_factory: PF,
55
56    /// Configuration for this phase.
57    config: PartitionedSearchConfig,
58
59    _marker: PhantomData<fn(S, D, PD, CP)>,
60}
61
62impl<S, D, PD, Part, SDF, PF, CP> PartitionedSearchPhase<S, D, PD, Part, SDF, PF, CP>
63where
64    S: PlanningSolution,
65    D: Director<S>,
66    PD: Director<S>,
67    Part: SolutionPartitioner<S>,
68    SDF: Fn(S) -> PD + Send + Sync,
69    PF: Fn() -> CP + Send + Sync,
70    CP: ChildPhases<S, PD>,
71{
72    /// Creates a new partitioned search phase.
73    pub fn new(partitioner: Part, score_director_factory: SDF, phase_factory: PF) -> Self {
74        Self {
75            partitioner,
76            score_director_factory,
77            phase_factory,
78            config: PartitionedSearchConfig::default(),
79            _marker: PhantomData,
80        }
81    }
82
83    /// Creates a partitioned search phase with custom configuration.
84    pub fn with_config(
85        partitioner: Part,
86        score_director_factory: SDF,
87        phase_factory: PF,
88        config: PartitionedSearchConfig,
89    ) -> Self {
90        Self {
91            partitioner,
92            score_director_factory,
93            phase_factory,
94            config,
95            _marker: PhantomData,
96        }
97    }
98}
99
100impl<S, D, PD, Part, SDF, PF, CP> Debug for PartitionedSearchPhase<S, D, PD, Part, SDF, PF, CP>
101where
102    S: PlanningSolution,
103    D: Director<S>,
104    PD: Director<S>,
105    Part: SolutionPartitioner<S> + Debug,
106    SDF: Fn(S) -> PD + Send + Sync,
107    PF: Fn() -> CP + Send + Sync,
108    CP: ChildPhases<S, PD>,
109{
110    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111        f.debug_struct("PartitionedSearchPhase")
112            .field("partitioner", &self.partitioner)
113            .field("config", &self.config)
114            .finish()
115    }
116}
117
118impl<S, D, BestCb, PD, Part, SDF, PF, CP> Phase<S, D, BestCb>
119    for PartitionedSearchPhase<S, D, PD, Part, SDF, PF, CP>
120where
121    S: PlanningSolution + 'static,
122    D: Director<S>,
123    BestCb: BestSolutionCallback<S>,
124    PD: Director<S> + 'static,
125    Part: SolutionPartitioner<S>,
126    SDF: Fn(S) -> PD + Send + Sync,
127    PF: Fn() -> CP + Send + Sync,
128    CP: ChildPhases<S, PD> + Send,
129{
130    fn solve(&mut self, solver_scope: &mut SolverScope<S, D, BestCb>) {
131        // Get the current solution
132        let solution = solver_scope.score_director().working_solution().clone();
133
134        // Partition the solution
135        let partitions = self.partitioner.partition(&solution);
136        let partition_count = partitions.len();
137
138        if partition_count == 0 {
139            return;
140        }
141
142        // Determine thread count
143        let thread_count = self.config.thread_count.resolve(partition_count);
144
145        if self.config.log_progress {
146            tracing::info!(event = "phase_start", phase = "PartitionedSearch",);
147        }
148
149        // Solve partitions - parallel when multiple, sequential when single
150        let solved_partitions: Vec<S> = if thread_count == 1 || partition_count == 1 {
151            partitions
152                .into_iter()
153                .map(|p| self.solve_partition(p))
154                .collect()
155        } else {
156            partitions
157                .into_par_iter()
158                .map(|partition| {
159                    let director = (self.score_director_factory)(partition);
160                    let mut solver_scope = SolverScope::new(director);
161                    let mut phases = (self.phase_factory)();
162                    phases.solve_all(&mut solver_scope);
163                    solver_scope.take_best_or_working_solution()
164                })
165                .collect()
166        };
167
168        // Merge the solved partitions
169        let merged = self.partitioner.merge(&solution, solved_partitions);
170
171        // Update the working solution with the merged result
172        let director = solver_scope.score_director_mut();
173
174        // Replace working solution with merged result
175        let working = director.working_solution_mut();
176        *working = merged;
177
178        // Calculate the score of the merged solution
179        solver_scope.calculate_score();
180
181        // Update best solution
182        solver_scope.update_best_solution();
183
184        if self.config.log_progress {
185            if let Some(score) = solver_scope.best_score() {
186                tracing::info!(
187                    event = "phase_end",
188                    phase = "PartitionedSearch",
189                    score = %format!("{:?}", score),
190                );
191            }
192        }
193    }
194
195    fn phase_type_name(&self) -> &'static str {
196        "PartitionedSearch"
197    }
198}
199
200impl<S, D, PD, Part, SDF, PF, CP> PartitionedSearchPhase<S, D, PD, Part, SDF, PF, CP>
201where
202    S: PlanningSolution,
203    D: Director<S>,
204    PD: Director<S>,
205    Part: SolutionPartitioner<S>,
206    SDF: Fn(S) -> PD + Send + Sync,
207    PF: Fn() -> CP + Send + Sync,
208    CP: ChildPhases<S, PD>,
209{
210    /// Solves a single partition and returns the solved solution.
211    fn solve_partition(&self, partition: S) -> S {
212        // Create score director for this partition
213        let director = (self.score_director_factory)(partition);
214
215        // Create solver scope
216        let mut solver_scope = SolverScope::new(director);
217
218        // Create and run child phases
219        let mut phases = (self.phase_factory)();
220        phases.solve_all(&mut solver_scope);
221
222        // Return the best solution (or working solution if no best)
223        solver_scope.take_best_or_working_solution()
224    }
225}
226
227#[cfg(test)]
228mod tests {
229    use super::super::partitioner::ThreadCount;
230    use super::*;
231
232    #[test]
233    fn test_config_default() {
234        let config = PartitionedSearchConfig::default();
235        assert_eq!(config.thread_count, ThreadCount::Auto);
236        assert!(!config.log_progress);
237    }
238}