Skip to main content

solverforge_solver/heuristic/selector/scalar_neighborhood/
spec.rs

1use std::fmt;
2
3use solverforge_config::{MoveSelectorConfig, RecreateHeuristicType};
4use solverforge_core::domain::PlanningSolution;
5
6use crate::builder::{RuntimeScalarSlot, RuntimeScalarSlotId, ScalarAccessCapability};
7
8/// One non-grouped scalar neighborhood family.
9///
10/// This discriminator belongs to the shared leaf kernel rather than a
11/// compiler/executor module: static facades, dynamic facades, and compiled
12/// declarations all describe the same seven families.
13#[derive(Clone, Copy, Debug, PartialEq, Eq)]
14pub enum ScalarNeighborhoodKind {
15    Change,
16    Swap,
17    NearbyChange,
18    NearbySwap,
19    PillarChange,
20    PillarSwap,
21    RuinRecreate,
22}
23
24impl ScalarNeighborhoodKind {
25    pub(crate) fn required_capability(self) -> Option<ScalarAccessCapability> {
26        match self {
27            Self::Change | Self::PillarChange | Self::PillarSwap | Self::RuinRecreate => {
28                Some(ScalarAccessCapability::Candidates)
29            }
30            Self::NearbyChange => Some(ScalarAccessCapability::NearbyValue),
31            Self::NearbySwap => Some(ScalarAccessCapability::NearbyEntity),
32            Self::Swap => None,
33        }
34    }
35
36    pub(crate) fn selector_name(self) -> &'static str {
37        match self {
38            Self::Change => "change_move_selector",
39            Self::Swap => "swap_move_selector",
40            Self::NearbyChange => "nearby_change_move_selector",
41            Self::NearbySwap => "nearby_swap_move_selector",
42            Self::PillarChange => "pillar_change_move_selector",
43            Self::PillarSwap => "pillar_swap_move_selector",
44            Self::RuinRecreate => "ruin_recreate_move_selector",
45        }
46    }
47}
48
49/// Frozen settings for one scalar leaf. It excludes target matching and
50/// composition: those belong to the model/compiler and generic composer.
51#[derive(Clone, Copy, Debug, PartialEq, Eq)]
52pub(crate) enum ScalarNeighborhoodSpec {
53    Change {
54        value_candidate_limit: Option<usize>,
55    },
56    Swap,
57    NearbyChange {
58        max_nearby: usize,
59        value_candidate_limit: Option<usize>,
60    },
61    NearbySwap {
62        max_nearby: usize,
63    },
64    PillarChange {
65        minimum_sub_pillar_size: usize,
66        maximum_sub_pillar_size: usize,
67        value_candidate_limit: Option<usize>,
68    },
69    PillarSwap {
70        minimum_sub_pillar_size: usize,
71        maximum_sub_pillar_size: usize,
72    },
73    RuinRecreate {
74        min_ruin_count: usize,
75        max_ruin_count: usize,
76        moves_per_step: usize,
77        value_candidate_limit: Option<usize>,
78        recreate_heuristic_type: RecreateHeuristicType,
79    },
80}
81
82impl ScalarNeighborhoodSpec {
83    pub(crate) fn from_config(
84        kind: ScalarNeighborhoodKind,
85        config: &MoveSelectorConfig,
86    ) -> Result<Self, ScalarNeighborhoodBindingError> {
87        let spec = match (kind, config) {
88            (ScalarNeighborhoodKind::Change, MoveSelectorConfig::ChangeMoveSelector(config)) => {
89                Self::Change {
90                    value_candidate_limit: config.value_candidate_limit,
91                }
92            }
93            (ScalarNeighborhoodKind::Swap, MoveSelectorConfig::SwapMoveSelector(_)) => Self::Swap,
94            (
95                ScalarNeighborhoodKind::NearbyChange,
96                MoveSelectorConfig::NearbyChangeMoveSelector(config),
97            ) => Self::NearbyChange {
98                max_nearby: config.max_nearby,
99                value_candidate_limit: config.value_candidate_limit,
100            },
101            (
102                ScalarNeighborhoodKind::NearbySwap,
103                MoveSelectorConfig::NearbySwapMoveSelector(config),
104            ) => Self::NearbySwap {
105                max_nearby: config.max_nearby,
106            },
107            (
108                ScalarNeighborhoodKind::PillarChange,
109                MoveSelectorConfig::PillarChangeMoveSelector(config),
110            ) => Self::PillarChange {
111                minimum_sub_pillar_size: config.minimum_sub_pillar_size,
112                maximum_sub_pillar_size: config.maximum_sub_pillar_size,
113                value_candidate_limit: config.value_candidate_limit,
114            },
115            (
116                ScalarNeighborhoodKind::PillarSwap,
117                MoveSelectorConfig::PillarSwapMoveSelector(config),
118            ) => Self::PillarSwap {
119                minimum_sub_pillar_size: config.minimum_sub_pillar_size,
120                maximum_sub_pillar_size: config.maximum_sub_pillar_size,
121            },
122            (
123                ScalarNeighborhoodKind::RuinRecreate,
124                MoveSelectorConfig::RuinRecreateMoveSelector(config),
125            ) => {
126                if config.min_ruin_count == 0 || config.max_ruin_count < config.min_ruin_count {
127                    return Err(ScalarNeighborhoodBindingError::InvalidRuinBounds {
128                        min_ruin_count: config.min_ruin_count,
129                        max_ruin_count: config.max_ruin_count,
130                    });
131                }
132                Self::RuinRecreate {
133                    min_ruin_count: config.min_ruin_count,
134                    max_ruin_count: config.max_ruin_count,
135                    moves_per_step: config.moves_per_step.unwrap_or(10).max(1),
136                    value_candidate_limit: config.value_candidate_limit,
137                    recreate_heuristic_type: config.recreate_heuristic_type,
138                }
139            }
140            _ => return Err(ScalarNeighborhoodBindingError::ConfigFamilyMismatch { kind }),
141        };
142        Ok(spec)
143    }
144
145    pub(crate) fn kind(self) -> ScalarNeighborhoodKind {
146        match self {
147            Self::Change { .. } => ScalarNeighborhoodKind::Change,
148            Self::Swap => ScalarNeighborhoodKind::Swap,
149            Self::NearbyChange { .. } => ScalarNeighborhoodKind::NearbyChange,
150            Self::NearbySwap { .. } => ScalarNeighborhoodKind::NearbySwap,
151            Self::PillarChange { .. } => ScalarNeighborhoodKind::PillarChange,
152            Self::PillarSwap { .. } => ScalarNeighborhoodKind::PillarSwap,
153            Self::RuinRecreate { .. } => ScalarNeighborhoodKind::RuinRecreate,
154        }
155    }
156}
157
158/// One selected scalar recipe. The recipe owns the frozen physical slot so a
159/// selected candidate never borrows a cursor, callback view, or row buffer.
160#[derive(Clone, Debug)]
161pub(crate) enum RuntimeScalarRecipe<S> {
162    Change {
163        slot: RuntimeScalarSlot<S>,
164        entity_index: usize,
165        to_value: Option<usize>,
166    },
167    Swap {
168        slot: RuntimeScalarSlot<S>,
169        left_entity_index: usize,
170        right_entity_index: usize,
171    },
172    PillarChange {
173        slot: RuntimeScalarSlot<S>,
174        entity_indices: Vec<usize>,
175        to_value: Option<usize>,
176    },
177    PillarSwap {
178        slot: RuntimeScalarSlot<S>,
179        left_indices: Vec<usize>,
180        right_indices: Vec<usize>,
181    },
182    RuinRecreate {
183        slot: RuntimeScalarSlot<S>,
184        entity_indices: Vec<usize>,
185        value_candidate_limit: Option<usize>,
186        recreate_heuristic_type: RecreateHeuristicType,
187    },
188}
189
190/// Structural leaf-creation failures. Direct dynamic APIs and compiler
191/// lowering use this same type so a missing nearby source never becomes an
192/// alternate candidate universe.
193#[derive(Clone, Debug, PartialEq, Eq)]
194pub enum ScalarNeighborhoodBindingError {
195    ConfigFamilyMismatch {
196        kind: ScalarNeighborhoodKind,
197    },
198    MissingCapability {
199        slot: RuntimeScalarSlotId,
200        capability: ScalarAccessCapability,
201    },
202    InvalidRuinBounds {
203        min_ruin_count: usize,
204        max_ruin_count: usize,
205    },
206}
207
208impl ScalarNeighborhoodBindingError {
209    pub(crate) fn validate_slot<S>(
210        kind: ScalarNeighborhoodKind,
211        slot: &RuntimeScalarSlot<S>,
212    ) -> Result<(), Self>
213    where
214        S: PlanningSolution,
215    {
216        let Some(capability) = kind.required_capability() else {
217            return Ok(());
218        };
219        if slot.has_capability(capability) {
220            Ok(())
221        } else {
222            Err(Self::MissingCapability {
223                slot: slot.id(),
224                capability,
225            })
226        }
227    }
228}
229
230impl fmt::Display for ScalarNeighborhoodBindingError {
231    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
232        match self {
233            Self::ConfigFamilyMismatch { kind } => {
234                write!(
235                    f,
236                    "{} received another selector configuration family",
237                    kind.selector_name()
238                )
239            }
240            Self::MissingCapability { slot, capability } => {
241                write!(
242                    f,
243                    "{slot} does not provide required {}",
244                    capability_label(*capability)
245                )
246            }
247            Self::InvalidRuinBounds {
248                min_ruin_count,
249                max_ruin_count,
250            } => write!(
251                f,
252                "ruin_recreate bounds require 1 <= min <= max, got {min_ruin_count}..={max_ruin_count}"
253            ),
254        }
255    }
256}
257
258impl std::error::Error for ScalarNeighborhoodBindingError {}
259
260fn capability_label(capability: ScalarAccessCapability) -> &'static str {
261    match capability {
262        ScalarAccessCapability::Candidates => "scalar candidate source",
263        ScalarAccessCapability::NearbyValue => "nearby scalar value source",
264        ScalarAccessCapability::NearbyEntity => "nearby scalar entity source",
265        ScalarAccessCapability::ConstructionEntityOrder => "construction entity-order source",
266        ScalarAccessCapability::ConstructionValueOrder => "construction value-order source",
267    }
268}