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    pub fn new(partitioner: Part, score_director_factory: SDF, phase_factory: PF) -> Self {
73        Self {
74            partitioner,
75            score_director_factory,
76            phase_factory,
77            config: PartitionedSearchConfig::default(),
78            _marker: PhantomData,
79        }
80    }
81
82    pub fn with_config(
83        partitioner: Part,
84        score_director_factory: SDF,
85        phase_factory: PF,
86        config: PartitionedSearchConfig,
87    ) -> Self {
88        Self {
89            partitioner,
90            score_director_factory,
91            phase_factory,
92            config,
93            _marker: PhantomData,
94        }
95    }
96}
97
98impl<S, D, PD, Part, SDF, PF, CP> Debug for PartitionedSearchPhase<S, D, PD, Part, SDF, PF, CP>
99where
100    S: PlanningSolution,
101    D: Director<S>,
102    PD: Director<S>,
103    Part: SolutionPartitioner<S> + Debug,
104    SDF: Fn(S) -> PD + Send + Sync,
105    PF: Fn() -> CP + Send + Sync,
106    CP: ChildPhases<S, PD>,
107{
108    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109        f.debug_struct("PartitionedSearchPhase")
110            .field("partitioner", &self.partitioner)
111            .field("config", &self.config)
112            .finish()
113    }
114}
115
116impl<S, D, BestCb, PD, Part, SDF, PF, CP> Phase<S, D, BestCb>
117    for PartitionedSearchPhase<S, D, PD, Part, SDF, PF, CP>
118where
119    S: PlanningSolution + 'static,
120    D: Director<S>,
121    BestCb: BestSolutionCallback<S>,
122    PD: Director<S> + 'static,
123    Part: SolutionPartitioner<S>,
124    SDF: Fn(S) -> PD + Send + Sync,
125    PF: Fn() -> CP + Send + Sync,
126    CP: ChildPhases<S, PD> + Send,
127{
128    fn solve(&mut self, solver_scope: &mut SolverScope<S, D, BestCb>) {
129        // Get the current solution
130        let solution = solver_scope.score_director().working_solution().clone();
131
132        // Partition the solution
133        let partitions = self.partitioner.partition(&solution);
134        let partition_count = partitions.len();
135
136        if partition_count == 0 {
137            return;
138        }
139
140        // Determine thread count
141        let thread_count = self.config.thread_count.resolve(partition_count);
142
143        if self.config.log_progress {
144            tracing::info!(event = "phase_start", phase = "PartitionedSearch",);
145        }
146
147        // Solve partitions - parallel when multiple, sequential when single
148        let solved_partitions: Vec<S> = if thread_count == 1 || partition_count == 1 {
149            partitions
150                .into_iter()
151                .map(|p| self.solve_partition(p))
152                .collect()
153        } else {
154            partitions
155                .into_par_iter()
156                .map(|partition| {
157                    let director = (self.score_director_factory)(partition);
158                    let mut solver_scope = SolverScope::new(director);
159                    let mut phases = (self.phase_factory)();
160                    phases.solve_all(&mut solver_scope);
161                    solver_scope.take_best_or_working_solution()
162                })
163                .collect()
164        };
165
166        // Merge the solved partitions
167        let merged = self.partitioner.merge(&solution, solved_partitions);
168
169        // Update the working solution with the merged result
170        let director = solver_scope.score_director_mut();
171
172        // Replace working solution with merged result
173        let working = director.working_solution_mut();
174        *working = merged;
175
176        // Calculate the score of the merged solution
177        solver_scope.calculate_score();
178
179        // Update best solution
180        solver_scope.update_best_solution();
181
182        if self.config.log_progress {
183            if let Some(score) = solver_scope.best_score() {
184                tracing::info!(
185                    event = "phase_end",
186                    phase = "PartitionedSearch",
187                    score = %format!("{:?}", score),
188                );
189            }
190        }
191    }
192
193    fn phase_type_name(&self) -> &'static str {
194        "PartitionedSearch"
195    }
196}
197
198impl<S, D, PD, Part, SDF, PF, CP> PartitionedSearchPhase<S, D, PD, Part, SDF, PF, CP>
199where
200    S: PlanningSolution,
201    D: Director<S>,
202    PD: Director<S>,
203    Part: SolutionPartitioner<S>,
204    SDF: Fn(S) -> PD + Send + Sync,
205    PF: Fn() -> CP + Send + Sync,
206    CP: ChildPhases<S, PD>,
207{
208    // Solves a single partition and returns the solved solution.
209    fn solve_partition(&self, partition: S) -> S {
210        // Create score director for this partition
211        let director = (self.score_director_factory)(partition);
212
213        // Create solver scope
214        let mut solver_scope = SolverScope::new(director);
215
216        // Create and run child phases
217        let mut phases = (self.phase_factory)();
218        phases.solve_all(&mut solver_scope);
219
220        // Return the best solution (or working solution if no best)
221        solver_scope.take_best_or_working_solution()
222    }
223}
224
225#[cfg(test)]
226mod tests {
227    use super::super::partitioner::ThreadCount;
228    use super::*;
229
230    #[test]
231    fn test_config_default() {
232        let config = PartitionedSearchConfig::default();
233        assert_eq!(config.thread_count, ThreadCount::Auto);
234        assert!(!config.log_progress);
235    }
236}