Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
a103070
Add FUNDING.md
reinterpretcat Dec 15, 2025
ab5ae8a
Add test to reproduce crash
reinterpretcat Dec 25, 2025
110a812
Fix crash with required break and relations
reinterpretcat Dec 25, 2025
ca23999
Improve format_time error handling
reinterpretcat Dec 25, 2025
12e66bf
Update to rust 1.92
reinterpretcat Dec 26, 2025
7b86780
Improve SISR implementation
reinterpretcat Jan 8, 2026
12422e0
Increase SISR probability
reinterpretcat Jan 8, 2026
10d46e9
Fix potential issue in lkh search
reinterpretcat Jan 9, 2026
5d3ce5c
Apply some tweaks in dynamic heuristic
reinterpretcat Jan 9, 2026
3c268c6
Fix clippy warning
reinterpretcat Jan 9, 2026
afb26d5
Redesign dynamic selective heuristic
reinterpretcat Jan 10, 2026
604c7e1
Change reward multiplier calculations
reinterpretcat Jan 11, 2026
0cc6ed6
Improve predictability of dynamic heuristic
reinterpretcat Jan 11, 2026
647d4e6
Improve comments
reinterpretcat Jan 11, 2026
5ac3ce3
Improve heuristic operators list
reinterpretcat Jan 11, 2026
bf82f5f
Fix broken tests
reinterpretcat Jan 11, 2026
0de8b35
Update changelog
reinterpretcat Jan 11, 2026
ba8e3ba
Fix clippy warnings
reinterpretcat Jan 11, 2026
a832861
Improve UI for heuristic playground
reinterpretcat Jan 12, 2026
07bad8a
Support loading of raw experimental output for dynamic heuristic
reinterpretcat Jan 13, 2026
725aa17
Improve a bit code
reinterpretcat Jan 13, 2026
d9446e8
Apply some refactorings and fix clippy warning
reinterpretcat Jan 13, 2026
ba470dc
Add into iterators for getting individuals from context
reinterpretcat Jan 13, 2026
01a7516
Modify decompose search to diversify solution if no improvement
reinterpretcat Jan 13, 2026
4ba697b
Fix clippy warning
reinterpretcat Jan 13, 2026
baec260
Add some improvements on heuristics
reinterpretcat Jan 14, 2026
86c9cac
Improve dynamic heuristic
reinterpretcat Jan 14, 2026
29f7884
Split decompose search
reinterpretcat Jan 14, 2026
49e1878
Fix clippy warnings
reinterpretcat Jan 14, 2026
6b50a83
Delete dead code
reinterpretcat Jan 14, 2026
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
1 change: 1 addition & 0 deletions .github/FUNDING.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
github: reinterpretcat
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ are already published. So, I stick to it for now.
* improved remedian algorithm
* separate calculations for distance/duration from cost minimization
* change GSOM distance function
* improve SISR implementation
* improve dynamic selective heuristic

### Added

Expand Down
4 changes: 2 additions & 2 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM rust:1.88-alpine AS Builder
FROM rust:1.92-alpine AS builder

LABEL maintainer="Ilya Builuk <ilya.builuk@gmail.com>" \
org.opencontainers.image.title="A Vehicle Routing Problem solver CLI" \
Expand Down Expand Up @@ -29,6 +29,6 @@ FROM alpine:3.18
ENV SOLVER_DIR=/solver

RUN mkdir $SOLVER_DIR
COPY --from=Builder /src/target/release/vrp-cli $SOLVER_DIR/vrp-cli
COPY --from=builder /src/target/release/vrp-cli $SOLVER_DIR/vrp-cli

WORKDIR $SOLVER_DIR
1 change: 1 addition & 0 deletions experiments/heuristic-research/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ pub fn run_vrp_experiment(format_type: &str, problem: &str, population_type: &st
/// Loads experiment data from json serialized representation.
#[wasm_bindgen]
pub fn load_state(data: &str) -> usize {
set_panic_hook_once();
match ExperimentData::try_from(data) {
Ok(data) => *EXPERIMENT_DATA.lock().unwrap() = data,
Err(err) => web_sys::console::log_1(&err.into()),
Expand Down
21 changes: 17 additions & 4 deletions experiments/heuristic-research/src/plots/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,9 @@ fn get_search_config(generation: usize, kind: &str) -> SearchDrawConfig {
let names_rev = data.heuristic_state.names.iter().map(|(k, v)| (*v, k)).collect::<HashMap<_, _>>();
let _states_rev = data.heuristic_state.states.iter().map(|(k, v)| (*v, k)).collect::<HashMap<_, _>>();

data.heuristic_state.search_states.get(&generation).map(|states| {
// Find nearest available generation if exact match doesn't exist
let nearest_gen = find_nearest_generation(&data.heuristic_state.search_states, generation);
data.heuristic_state.search_states.get(&nearest_gen).map(|states| {
let estimations = states
.iter()
// NOTE: just show all transitions
Expand All @@ -264,7 +266,7 @@ fn get_search_config(generation: usize, kind: &str) -> SearchDrawConfig {
get_search_statistics(
&data.heuristic_state.search_states,
&names_rev,
generation,
nearest_gen,
|SearchResult(_, _, (_, to_state_idx), _)| to_state_idx == best_state_idx,
|acc, SearchResult(name_idx, ..)| {
acc[*name_idx] += 1;
Expand All @@ -276,7 +278,7 @@ fn get_search_config(generation: usize, kind: &str) -> SearchDrawConfig {
let overall = get_search_statistics(
&data.heuristic_state.search_states,
&names_rev,
generation,
nearest_gen,
|_| true,
|acc, SearchResult(name_idx, ..)| {
acc[*name_idx] += 1;
Expand All @@ -287,7 +289,7 @@ fn get_search_config(generation: usize, kind: &str) -> SearchDrawConfig {
let durations = get_search_statistics(
&data.heuristic_state.search_states,
&names_rev,
generation,
nearest_gen,
|_| true,
|acc: &mut Vec<(usize, usize)>, SearchResult(name_idx, _, _, duration)| {
let (total, count) = (acc[*name_idx].0, acc[*name_idx].1);
Expand All @@ -305,6 +307,17 @@ fn get_search_config(generation: usize, kind: &str) -> SearchDrawConfig {
.unwrap_or_default()
}

/// Finds the nearest available generation that is <= the requested generation.
/// Returns the requested generation if it exists, otherwise the closest smaller one.
fn find_nearest_generation(generations: &HashMap<usize, Vec<SearchResult>>, requested: usize) -> usize {
if generations.contains_key(&requested) {
return requested;
}

// Find the largest generation that is smaller than requested
generations.keys().filter(|&&g| g <= requested).copied().max().unwrap_or(requested)
}

fn get_search_statistics<T, FF, AF>(
search_states: &HashMap<usize, Vec<SearchResult>>,
names_rev: &HashMap<usize, &String>,
Expand Down
33 changes: 30 additions & 3 deletions experiments/heuristic-research/src/solver/proxies.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,29 @@ impl<'a> TryFrom<&'a str> for ExperimentData {
type Error = String;

fn try_from(value: &'a str) -> Result<Self, Self::Error> {
// Check if this is telemetry CSV format (contains "TELEMETRY" somewhere in the content)
// Extract telemetry section if present, otherwise try JSON
if let Some(telemetry_start) = value.find("TELEMETRY") {
// Extract everything from TELEMETRY onwards
let telemetry_content = &value[telemetry_start..];

// Parse telemetry CSV using existing parser
let heuristic_state = HyperHeuristicState::try_parse_all(telemetry_content)
.ok_or_else(|| "Failed to parse telemetry data".to_string())?;

// Find max generation from telemetry data
let generation = heuristic_state
.search_states
.keys()
.chain(heuristic_state.heuristic_states.keys())
.copied()
.max()
.unwrap_or(0);

return Ok(ExperimentData { heuristic_state, generation, ..Default::default() });
}

// Try parsing as JSON
serde_json::from_str(value).map_err(|err| format!("cannot deserialize experiment data: {err}"))
}
}
Expand Down Expand Up @@ -129,7 +152,7 @@ where
self.generation = statistics.generation;
self.acquire().generation = statistics.generation;

let individuals_data = self.inner.all().map(|individual| individual.into()).collect::<Vec<_>>();
let individuals_data = self.inner.iter().map(|individual| individual.into()).collect::<Vec<_>>();
// NOTE this is not exactly how footprint is calculated in production code.
// In the production version, approximation of the footprint is used to avoid iterating over
// all individuals in the population on generation update.
Expand Down Expand Up @@ -164,8 +187,12 @@ where
self.inner.ranked()
}

fn all(&self) -> Box<dyn Iterator<Item = &'_ Self::Individual> + '_> {
self.inner.all()
fn iter(&self) -> Box<dyn Iterator<Item = &'_ Self::Individual> + '_> {
self.inner.iter()
}

fn into_iter(self: Box<Self>) -> Box<dyn Iterator<Item = Self::Individual>> {
Box::new(self.inner).into_iter()
}

fn size(&self) -> usize {
Expand Down
51 changes: 26 additions & 25 deletions experiments/heuristic-research/src/solver/state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,31 +172,32 @@ impl HyperHeuristicState {
.values_mut()
.for_each(|states| states.sort_by(|SearchResult(a, ..), SearchResult(b, ..)| a.cmp(b)));

let mut heuristic_states = data.lines().skip_while(|line| *line != "heuristic:").skip(2).fold(
HashMap::<_, Vec<_>>::new(),
|mut data, line| {
let fields: Vec<String> = line.split(',').map(|s| s.to_string()).collect();

let generation: usize = fields[0].parse().unwrap();
let state = fields[1].clone();
let name = fields[2].clone();
let alpha = fields[3].parse().unwrap();
let beta = fields[4].parse().unwrap();
let mu = fields[5].parse().unwrap();
let v = fields[6].parse().unwrap();
let n = fields[7].parse().unwrap();

insert_to_map(&mut states, state.clone());
insert_to_map(&mut names, name.clone());

let state = states.get(&state).copied().unwrap();
let name = names.get(&name).copied().unwrap();

data.entry(generation).or_default().push(HeuristicResult(state, name, alpha, beta, mu, v, n));

data
},
);
let mut heuristic_states =
data.lines().skip_while(|line| *line != "heuristic:").skip(2).take_while(|line| !line.is_empty()).fold(
HashMap::<_, Vec<_>>::new(),
|mut data, line| {
let fields: Vec<String> = line.split(',').map(|s| s.to_string()).collect();

let generation: usize = fields[0].parse().unwrap();
let state = fields[1].clone();
let name = fields[2].clone();
let alpha = fields[3].parse().unwrap();
let beta = fields[4].parse().unwrap();
let mu = fields[5].parse().unwrap();
let v = fields[6].parse().unwrap();
let n = fields[7].parse().unwrap();

insert_to_map(&mut states, state.clone());
insert_to_map(&mut names, name.clone());

let state = states.get(&state).copied().unwrap();
let name = names.get(&name).copied().unwrap();

data.entry(generation).or_default().push(HeuristicResult(state, name, alpha, beta, mu, v, n));

data
},
);
heuristic_states
.values_mut()
.for_each(|states| states.sort_by(|HeuristicResult(_, a, ..), HeuristicResult(_, b, ..)| a.cmp(b)));
Expand Down
6 changes: 3 additions & 3 deletions experiments/heuristic-research/src/solver/vrp/population.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ pub fn get_population_fitness_fn(generation: usize) -> FitnessFn {
.lock()
.ok()
.and_then(|data| data.on_generation.get(&generation).map(|(footprint, _)| footprint.clone()))
.map(|footprint| {
.map(|footprint| -> FitnessFn {
Arc::new(move |input: &[Float]| {
if let &[from, to] = input {
footprint.get(from as usize, to as usize) as Float
Expand All @@ -19,7 +19,7 @@ pub fn get_population_fitness_fn(generation: usize) -> FitnessFn {
}
})
})
.expect("cannot get data from EXPERIMENT_DATA")
.unwrap_or_else(|| Arc::new(|_: &[Float]| 0.0) as FitnessFn)
}

/// Returns a description of population state.
Expand All @@ -32,5 +32,5 @@ pub fn get_population_desc(generation: usize) -> String {
format!("total [{}], known edges [{}]", individuals.len(), footprint.desc())
})
})
.expect("cannot get data from EXPERIMENT_DATA")
.unwrap_or_else(|| String::from("no data"))
}
Loading