Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/solverforge-config/WIREFRAME.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ Derives: `Debug, Clone, Default, Deserialize, Serialize`.
| `construction_heuristic_type` | `ConstructionHeuristicType` | `FirstFit` |
| `construction_obligation` | `ConstructionObligation` | `PreserveUnassigned` |
| `target` | `VariableTargetConfig` | empty target |
| `k` | `usize` | `2` (for `ListKOpt`); valid: exactly `2` (the list k-opt construction kernel implements 2-opt only) |
| `k` | `usize` | `2`; valid: `2..=5` for `ListKOpt`, which polishes each route with k-opt; other heuristics do not use it and reject any value but `2` |
| `value_candidate_limit` | `Option<usize>` | `None`; valid: `>= 1` when set |
| `group_name` | `Option<String>` | `None` |
| `group_candidate_limit` | `Option<usize>` | `None`; valid: `>= 1` when set |
Expand Down
3 changes: 2 additions & 1 deletion crates/solverforge-config/src/phase.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ pub struct ConstructionHeuristicConfig {
#[serde(flatten)]
pub target: VariableTargetConfig,

/// k for ListKOpt (default 2).
/// Cuts per k-opt move of `list_k_opt` construction, from 2 to 5 (default
/// 2). Other heuristics do not use it, and setting it for them is an error.
#[serde(default = "default_k")]
pub k: usize,

Expand Down
3 changes: 3 additions & 0 deletions crates/solverforge-solver/WIREFRAME.md
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,9 @@ src/
│ │ ├── list_clarke_wright/tests/metric_class.rs — Shared metric-class and owner-feasibility tests
│ │ ├── list_clarke_wright/tests/owner_binding.rs — Owner-specific hook binding tests
│ │ ├── list_k_opt.rs — ListKOptPhase
│ │ ├── list_k_opt/kernel.rs — Route-local polishing loop and exact 2-opt
│ │ ├── list_k_opt/general.rs — Route-local k-opt for k from 3 to 5
│ │ ├── list_k_opt/facade.rs — Function-pointer test adapter
│ ├── mod_tests_integration.rs — Integration test module declarations
│ ├── mod_tests_integration/common.rs — Shared no-op phase fixture
│ ├── mod_tests_integration/gates.rs — Shared retained-job gates and blockers
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
//! Per-route k-opt polishing phase for list-variable problems.
//!
//! Applies 2-opt local search to each entity's route independently,
//! Applies k-opt local search (k from 2 to 5) to each entity's route independently,
//! improving solution quality after construction without modifying the
//! overall assignment structure.

Expand All @@ -12,6 +12,7 @@ use crate::heuristic::selector::nearby_list_change::CrossEntityDistanceMeter;

#[cfg(test)]
mod facade;
mod general;
mod kernel;

#[cfg(test)]
Expand Down Expand Up @@ -312,4 +313,40 @@ mod tests {
crate::manager::SolverTerminalReason::TerminatedByConfig
);
}

fn original_or_sorted(_: &Plan, _: usize, route: &[usize]) -> bool {
route == [1, 4, 5, 2, 3, 6] || route == [1, 2, 3, 4, 5, 6]
}

fn polish(k: usize) -> Vec<usize> {
let plan = Plan {
routes: vec![Route {
visits: vec![1, 4, 5, 2, 3, 6],
}],
score: None,
};
let mut solver_scope = SolverScope::new(director(plan));
let mut phase = ListKOptPhase::<Plan, usize>::new(
k,
entity_count,
get_route,
set_route,
depot,
line_distance,
Some(original_or_sorted),
0,
);
phase.solve(&mut solver_scope);
solver_scope.working_solution().routes[0].visits.clone()
}

#[test]
fn higher_k_moves_a_segment_that_no_reversal_reaches() {
// Sorting needs the segment [4, 5] moved behind [2, 3] unreversed:
// one 3-opt reconnection, and no single 2-opt reversal.
assert_eq!(polish(2), [1, 4, 5, 2, 3, 6]);
for k in 3..=5 {
assert_eq!(polish(k), [1, 2, 3, 4, 5, 6], "k = {k}");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,18 @@ use std::marker::PhantomData;

/// Per-route k-opt polishing phase for list variable problems.
///
/// Runs 2-opt local search on each entity's route to local optimum after
/// Runs k-opt local search on each entity's route to local optimum after
/// construction. All domain knowledge is supplied via function pointers.
///
/// # Algorithm (k=2, 2-opt)
/// # Algorithm
///
/// For each entity:
/// 1. Read the current route as a sequence of element indices.
/// 2. Try every segment reversal `[i..=j]`.
/// 3. Accept if distance improves AND `feasible_fn` passes (if provided).
/// 4. Repeat until no improving reversal exists.
/// 2. k = 2: try every segment reversal `[i..=j]`. k from 3 to 5: try every
/// reconnection pattern of every set of k cuts.
/// 3. Accept the first move that improves distance AND passes `feasible_fn`
/// (if provided).
/// 4. Repeat until no improving move exists.
pub(crate) struct ListKOptPhase<S, E>
where
S: PlanningSolution,
Expand Down Expand Up @@ -54,7 +56,7 @@ where
///
/// # Arguments
///
/// * `k` — k value; only k=2 (2-opt) is implemented; k>2 logs a warning and is a no-op
/// * `k` — cuts per move, from 2 to 5
/// * `entity_count` — number of entities (routes) in the solution
/// * `get_route` — returns the route for entity at given index as element indices
/// * `set_route` — replaces the route for entity at given index
Expand Down
Loading
Loading