Skip to main content

solverforge_solver/builder/
search.rs

1use std::fmt::Debug;
2use std::hash::Hash;
3
4use solverforge_core::domain::{PlanningSolution, SolutionDescriptor};
5use solverforge_core::score::{ParseableScore, Score};
6
7use crate::heuristic::r#move::Move;
8use crate::heuristic::selector::nearby_list_change::CrossEntityDistanceMeter;
9use crate::heuristic::MoveSelector;
10use crate::phase::localsearch::SelectorCursorSource;
11use crate::phase::localsearch::{Acceptor, LocalSearchForager, LocalSearchPhase};
12use crate::RuntimeBuildResult;
13
14use super::RuntimeModel;
15
16mod custom;
17
18pub use custom::{
19    CustomPhaseNode, CustomSearchPhase, NoDynamicExtensions, NoRuntimeExtensionPhase,
20    NoTypedExtensions, PartitionedPhaseNode, RuntimeExtensionPolicy, RuntimeExtensionRegistry,
21};
22
23pub struct SearchContext<
24    S,
25    V = usize,
26    DM = crate::heuristic::selector::DefaultCrossEntityDistanceMeter,
27    IDM = crate::heuristic::selector::DefaultCrossEntityDistanceMeter,
28> where
29    S: PlanningSolution,
30{
31    descriptor: SolutionDescriptor,
32    model: RuntimeModel<S, V, DM, IDM>,
33    random_seed: Option<u64>,
34}
35
36impl<S, V, DM, IDM> SearchContext<S, V, DM, IDM>
37where
38    S: PlanningSolution,
39{
40    pub fn new(
41        descriptor: SolutionDescriptor,
42        model: RuntimeModel<S, V, DM, IDM>,
43        random_seed: Option<u64>,
44    ) -> Self {
45        Self::try_new(descriptor, model, random_seed).unwrap_or_else(|error| match error {
46            crate::RuntimeBuildError::Declaration { message } => panic!("{message}"),
47            other => panic!("{other}"),
48        })
49    }
50
51    pub fn try_new(
52        descriptor: SolutionDescriptor,
53        model: RuntimeModel<S, V, DM, IDM>,
54        random_seed: Option<u64>,
55    ) -> RuntimeBuildResult<Self> {
56        let model = model
57            .resolve_dynamic_descriptor_indexes(&descriptor)
58            .map_err(crate::RuntimeBuildError::declaration)?;
59        Ok(Self {
60            descriptor,
61            model,
62            random_seed,
63        })
64    }
65
66    pub fn descriptor(&self) -> &SolutionDescriptor {
67        &self.descriptor
68    }
69
70    pub fn model(&self) -> &RuntimeModel<S, V, DM, IDM> {
71        &self.model
72    }
73
74    pub fn seed(&self) -> Option<u64> {
75        self.random_seed
76    }
77
78    pub fn defaults(self) -> SearchBuilder<S, V, DM, IDM, NoTypedExtensions> {
79        SearchBuilder {
80            context: self,
81            extensions: NoTypedExtensions,
82        }
83    }
84}
85
86/// Typed search authoring declaration consumed by the configured runtime.
87///
88/// Implementations declare one descriptor-resolved runtime model and one
89/// concrete typed-extension registry. The configured runner is the sole owner
90/// of graph compilation and per-solve phase preparation.
91pub trait Search<
92    S,
93    V = usize,
94    DM = crate::heuristic::selector::DefaultCrossEntityDistanceMeter,
95    IDM = crate::heuristic::selector::DefaultCrossEntityDistanceMeter,
96> where
97    S: PlanningSolution + 'static,
98    S::Score: Score + ParseableScore,
99    V: Clone + Copy + PartialEq + Eq + Hash + Into<usize> + Send + Sync + Debug + 'static,
100    DM: CrossEntityDistanceMeter<S> + Clone + Debug + Send + 'static,
101    IDM: CrossEntityDistanceMeter<S> + Clone + Debug + Send + 'static,
102{
103    /// Concrete typed extension registry transferred with this declaration.
104    type Extensions: RuntimeExtensionRegistry<S, V, DM, IDM>;
105
106    /// Transfers descriptor-resolved authoring to the configured runtime.
107    ///
108    /// This intentionally exposes declaration parts rather than the private
109    /// compiled graph. Bindings cannot construct, inspect, cache, or replace
110    /// runtime compiler input or prepared phases.
111    #[doc(hidden)]
112    fn into_runtime_parts(self) -> (SearchContext<S, V, DM, IDM>, Self::Extensions);
113}
114
115pub struct SearchBuilder<S, V, DM, IDM, Extensions>
116where
117    S: PlanningSolution,
118{
119    context: SearchContext<S, V, DM, IDM>,
120    extensions: Extensions,
121}
122
123impl<S, V, DM, IDM, Extensions> SearchBuilder<S, V, DM, IDM, Extensions>
124where
125    S: PlanningSolution,
126{
127    /// Transfers this typed authoring declaration without building phases.
128    pub fn into_runtime_parts(self) -> (SearchContext<S, V, DM, IDM>, Extensions) {
129        (self.context, self.extensions)
130    }
131}
132
133impl<S, V, DM, IDM, Extensions> SearchBuilder<S, V, DM, IDM, Extensions>
134where
135    S: PlanningSolution + 'static,
136    S::Score: Score + ParseableScore,
137    V: Clone + Copy + PartialEq + Eq + Hash + Into<usize> + Send + Sync + Debug + 'static,
138    DM: CrossEntityDistanceMeter<S> + Clone + Debug + Send + 'static,
139    IDM: CrossEntityDistanceMeter<S> + Clone + Debug + Send + 'static,
140    Extensions: RuntimeExtensionRegistry<S, V, DM, IDM>,
141{
142    pub fn phase<P, F>(
143        self,
144        name: &'static str,
145        builder: F,
146    ) -> SearchBuilder<S, V, DM, IDM, CustomPhaseNode<Extensions, F, P>>
147    where
148        F: Fn(&SearchContext<S, V, DM, IDM>) -> P + Send + Sync + 'static,
149        P: CustomSearchPhase<S> + 'static,
150    {
151        assert!(
152            !self.extensions.contains_custom(name) && !self.extensions.contains_partitioned(name),
153            "custom phase `{name}` was registered more than once",
154        );
155        SearchBuilder {
156            context: self.context,
157            extensions: CustomPhaseNode::new(self.extensions, name, builder),
158        }
159    }
160
161    pub fn partitioned_phase<P, F>(
162        self,
163        name: &'static str,
164        builder: F,
165    ) -> SearchBuilder<S, V, DM, IDM, PartitionedPhaseNode<Extensions, F, P>>
166    where
167        F: Fn(&SearchContext<S, V, DM, IDM>, &solverforge_config::PartitionedSearchConfig) -> P
168            + Send
169            + Sync
170            + 'static,
171        P: CustomSearchPhase<S> + 'static,
172    {
173        assert!(
174            !self.extensions.contains_custom(name) && !self.extensions.contains_partitioned(name),
175            "partitioned_search partitioner `{name}` was registered more than once",
176        );
177        SearchBuilder {
178            context: self.context,
179            extensions: PartitionedPhaseNode::new(self.extensions, name, builder),
180        }
181    }
182}
183
184impl<S, V, DM, IDM, Extensions> Search<S, V, DM, IDM> for SearchBuilder<S, V, DM, IDM, Extensions>
185where
186    S: PlanningSolution + 'static,
187    S::Score: Score + ParseableScore,
188    V: Clone + Copy + PartialEq + Eq + Hash + Into<usize> + Send + Sync + Debug + 'static,
189    DM: CrossEntityDistanceMeter<S> + Clone + Debug + Send + 'static,
190    IDM: CrossEntityDistanceMeter<S> + Clone + Debug + Send + 'static,
191    Extensions: RuntimeExtensionRegistry<S, V, DM, IDM>,
192{
193    type Extensions = Extensions;
194
195    fn into_runtime_parts(self) -> (SearchContext<S, V, DM, IDM>, Self::Extensions) {
196        SearchBuilder::into_runtime_parts(self)
197    }
198}
199
200pub fn local_search<S, M, MS, A, Fo>(
201    move_selector: MS,
202    acceptor: A,
203    forager: Fo,
204) -> LocalSearchPhase<S, M, SelectorCursorSource<MS>, A, Fo>
205where
206    S: PlanningSolution,
207    M: Move<S> + 'static,
208    MS: MoveSelector<S, M>,
209    A: Acceptor<S>,
210    Fo: LocalSearchForager<S, M>,
211{
212    LocalSearchPhase::new(move_selector, acceptor, forager, None)
213}
214
215#[cfg(test)]
216mod tests;