Skip to content

Commit

Permalink
refactor: simplify cache API
Browse files Browse the repository at this point in the history
decrease the number of arguments passed to the check function by converting a StartCandidate to a CacheItem beforehand, align the naming of variables
  • Loading branch information
moldhouse committed Mar 17, 2024
1 parent 646e4b6 commit 791137b
Show file tree
Hide file tree
Showing 4 changed files with 120 additions and 147 deletions.
181 changes: 77 additions & 104 deletions src/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,21 @@ use std::collections::HashSet;

pub struct CacheItem {
pub start: usize,
pub last_stop: usize,
pub stop_set: HashSet<usize>,
pub stops: HashSet<usize>,
pub max_stop: usize,
pub distance: f32,
}

impl CacheItem {
pub fn from_candidate(candidate: &StartCandidate, stops: HashSet<usize>) -> CacheItem {
let max_stop = *stops.iter().max().unwrap();
CacheItem {
start: candidate.start,
stops,
max_stop,
distance: 0.0,
}
}
// If the current stop set of an incoming item is super set of the stop set of the cached item,
// we can place an upper bound on the possible distance with the current stop set:
//
Expand All @@ -24,35 +33,34 @@ impl CacheItem {
// 3. The max distance of any stop in the current stop set (that is not in the cached stop set)
// to the last (or any single other) item in the cached stop set (the last leads to the lowest bound in the most cases)
// If this upper bound is lower than the current best distance, we can rule out the candidate.
//
//
// Note: If the possible endpoints of the incoming item would be a subset of the cached item, the maximum altitude of the
// stop set could be higher, therefore allowing for more start points then the cached item. In this case, the cached item
// can not be used to calculate an upper bound.
pub fn places_upperbound(
&self,
candidate: &mut CacheItem,
flat_points: &[FlatPoint<f32>],
candidate: &StartCandidate,
best_distance: f32,
stop_set: &HashSet<usize>,
) -> Option<f32> {
let offset_start = flat_points[self.start].distance(&flat_points[candidate.start_index]);
let mut candidate_guess = self.distance + offset_start;
if candidate_guess >= best_distance {
) -> bool {
let start_offset = flat_points[self.start].distance(&flat_points[candidate.start]);
candidate.distance = self.distance + start_offset;
if candidate.distance >= best_distance {
// this item does not provide an upper bound below best_distance
return None;
return false;
}
if !stop_set.is_superset(&self.stop_set) {
return None;
if !candidate.stops.is_superset(&self.stops) {
return false;
}
for to_check in stop_set.difference(&self.stop_set) {
let offset_end = flat_points[self.last_stop].distance(&flat_points[*to_check]);
let new_guess = offset_end + offset_start + self.distance;
candidate_guess = candidate_guess.max(new_guess);
if candidate_guess > best_distance {
return None;
for to_check in candidate.stops.difference(&self.stops) {
let stop_offset = flat_points[self.max_stop].distance(&flat_points[*to_check]);
let new_guess = stop_offset + start_offset + self.distance;
candidate.distance = candidate.distance.max(new_guess);
if candidate.distance > best_distance {
return false;
}
}
Some(candidate_guess)
true
}
}

Expand All @@ -72,27 +80,13 @@ impl Cache {

pub fn check(
&mut self,
candidate: &mut CacheItem,
flat_points: &[FlatPoint<f32>],
candidate: &StartCandidate,
best_distance: f32,
stop_set: &HashSet<usize>,
) -> bool {
// iterate in reverse order as it provides a speed-up on a broad test suite of files
for cache_item in self.items.iter().rev() {
if let Some(upperbound) =
cache_item.places_upperbound(flat_points, candidate, best_distance, stop_set)
{
// there is no need to add this to the cache, because the relation is transitive
// if A provides an upperbound for B, and B provides an upperbound for a later C
// then A provides an upperbound for C, so we don't need to add B to the cache
//
// BUT: adding this to the cache provides a speed-up on the test suite
self.set(CacheItem {
start: candidate.start_index,
last_stop: *stop_set.iter().max().unwrap(),
stop_set: stop_set.clone(),
distance: upperbound,
});
if cache_item.places_upperbound(candidate, flat_points, best_distance) {
return true;
}
}
Expand All @@ -106,97 +100,84 @@ mod tests {

#[test]
fn test_item_with_super_set_does_not_place_upperbound() {
// set a high best distance to make sure the cache item stays below
let flat_points = vec![FlatPoint { x: 0.0, y: 0.0 }, FlatPoint { x: 1.0, y: 1.0 }];
let candidate = StartCandidate {
start_index: 0,
let mut candidate = CacheItem {
start: 0,
stops: [0].into_iter().collect(),
max_stop: 0,
distance: 0.0,
};
let best_distance = 1_000.0;

let mut super_set: HashSet<_> = vec![0, 1].into_iter().collect();
let sub_set: HashSet<_> = vec![0].into_iter().collect();
super_set.insert(0);

// set the item with the super set in the cache
let item = CacheItem {
start: 0,
last_stop: 0,
stop_set: super_set,
max_stop: 0,
stops: [0, 1].into_iter().collect(),
distance: 0.0,
};

assert_eq!(
item.places_upperbound(&flat_points, &candidate, best_distance, &sub_set),
None
);
// set a high best distance to make sure the cache item stays below
let best_distance = 1_000.0;
assert!(!item.places_upperbound(&mut candidate, &flat_points, best_distance))
}

#[test]
fn test_item_with_sub_set_places_upperbound() {
let flat_points = vec![FlatPoint { x: 0.0, y: 0.0 }, FlatPoint { x: 1.0, y: 1.0 }];
let candidate = StartCandidate {
start_index: 0,
let mut candidate = CacheItem {
start: 0,
stops: [0, 1].into_iter().collect(),
max_stop: 1,
distance: 0.0,
};

// set a high best distance to make sure the cache item stays below
let best_distance = 1_000.0;

let mut super_set: HashSet<_> = vec![0, 1].into_iter().collect();
let sub_set: HashSet<_> = vec![0].into_iter().collect();
super_set.insert(0);

let item = CacheItem {
start: 0,
last_stop: 0,
stop_set: sub_set,
max_stop: 0,
stops: [0].into_iter().collect(),
distance: 0.0,
};
assert!(item
.places_upperbound(&flat_points, &candidate, best_distance, &super_set)
.is_some());

// set a high best distance to make sure the cache item stays below
let best_distance = 1_000.0;
assert!(item.places_upperbound(&mut candidate, &flat_points, best_distance));
}

#[test]
fn test_item_with_sub_set_but_bigger_distance() {
let flat_points = vec![FlatPoint { x: 0.0, y: 0.0 }, FlatPoint { x: 1.0, y: 1.0 }];
let candidate = StartCandidate {
start_index: 0,
let mut candidate = CacheItem {
start: 0,
stops: [0, 1].into_iter().collect(),
max_stop: 1,
distance: 100.0,
};
// set a high best distance to make sure the item exceeds this
let best_distance = 1.0;

let mut super_set: HashSet<_> = vec![0, 1].into_iter().collect();
let sub_set: HashSet<_> = vec![0].into_iter().collect();
super_set.insert(0);

let item = CacheItem {
start: 0,
last_stop: 0,
stop_set: sub_set,
max_stop: 0,
stops: [0].into_iter().collect(),
distance: 100.0,
};

assert!(item
.places_upperbound(&flat_points, &candidate, best_distance, &super_set)
.is_none());
// set a high best distance to make sure the item exceeds this
let best_distance = 1.0;
assert!(!item.places_upperbound(&mut candidate, &flat_points, best_distance))
}

#[test]
fn test_set_preserves_order() {
let mut cache = Cache::new();
let first_item = CacheItem {
start: 0,
last_stop: 1,
stop_set: HashSet::new(),
max_stop: 1,
stops: HashSet::new(),
distance: 0.0,
};
let second_item = CacheItem {
start: 1,
last_stop: 2,
stop_set: HashSet::new(),
max_stop: 2,
stops: HashSet::new(),
distance: 0.0,
};
cache.set(first_item);
Expand All @@ -207,46 +188,38 @@ mod tests {
#[test]
fn test_empty_cache_returns_false() {
let flat_points = vec![FlatPoint { x: 0.0, y: 0.0 }, FlatPoint { x: 1.0, y: 1.0 }];
let candidate = StartCandidate {
start_index: 0,
let mut candidate = CacheItem {
start: 0,
stops: HashSet::new(),
max_stop: 0,
distance: 0.0,
};
let best_distance = 0.0;
let stop_set = HashSet::new();

let mut cache = Cache::new();
assert_eq!(
cache.check(&flat_points, &candidate, best_distance, &stop_set),
false
);
assert!(!cache.check(&mut candidate, &flat_points, best_distance));
}

#[test]
fn test_cache_with_sub_set_item_returns_true() {
let flat_points = vec![FlatPoint { x: 0.0, y: 0.0 }, FlatPoint { x: 1.0, y: 1.0 }];
let candidate = StartCandidate {
start_index: 0,
let mut candidate = CacheItem {
start: 0,
stops: [0, 1].into_iter().collect(),
max_stop: 1,
distance: 0.0,
};

// set a high best distance to make sure the cache item stays below
let best_distance = 1_000.0;

let mut super_set: HashSet<_> = vec![0, 1].into_iter().collect();
let sub_set: HashSet<_> = vec![0].into_iter().collect();
super_set.insert(0);

let mut cache = Cache::new();
let item = CacheItem {
start: 0,
last_stop: 0,
stop_set: sub_set,
max_stop: 0,
stops: [0].into_iter().collect(),
distance: 0.0,
};
let mut cache = Cache::new();
cache.set(item);
assert_eq!(
cache.check(&flat_points, &candidate, best_distance, &super_set),
true
);

// set a high best distance to make sure the cache item stays below
let best_distance = 1_000.0;
assert!(cache.check(&mut candidate, &flat_points, best_distance));
}
}
28 changes: 15 additions & 13 deletions src/free.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
use flat_projection::FlatPoint;
use std::collections::HashSet;

use crate::cache::{Cache, CacheItem};
use crate::flat::to_flat_points;
Expand Down Expand Up @@ -40,7 +39,7 @@ pub fn optimize<T: Point>(route: &[T], break_at: f32, legs: usize) -> Option<Opt
}
}

let min_stop_idx = find_min_stop_idx(&dist_matrix, best_valid.distance);
let minimum_stop = find_minimum_stop(&dist_matrix, best_valid.distance);
let mut cache = Cache::new();

start_candidates.retain(|c| c.distance > best_valid.distance);
Expand All @@ -49,24 +48,27 @@ pub fn optimize<T: Point>(route: &[T], break_at: f32, legs: usize) -> Option<Opt
if candidate.distance < break_at {
return Some(best_valid);
}
let stops: Vec<usize> = candidate.get_valid_end_points(route, min_stop_idx);
let stops = candidate.get_valid_stops(route, minimum_stop);
if stops.is_empty() {
continue;
}
let stop_set: HashSet<usize> = stops.iter().cloned().collect();
if cache.check(&flat_points, &candidate, best_valid.distance, &stop_set) {
let mut to_check = CacheItem::from_candidate(&candidate, stops);
if cache.check(&mut to_check, &flat_points, best_valid.distance) {
// there is no need to add this to the cache, because the relation is transitive
// if A provides an upperbound for B, and B provides an upperbound for a later C
// then A provides an upperbound for C, so we don't need to add B to the cache
//
// BUT: adding this to the cache provides a speed-up on the test suite
cache.set(to_check);
continue;
}

// do the full (expensive) optimization
let candidate_graph = Graph::for_candidate(&candidate, &dist_matrix, route, legs);
let best_valid_for_candidate = candidate_graph.find_best_valid_solution(route);
let cache_item = CacheItem {
start: candidate.start_index,
last_stop: *stops.last().unwrap(),
stop_set,
distance: best_valid_for_candidate.distance,
};
cache.set(cache_item);

to_check.distance = best_valid_for_candidate.distance;
cache.set(to_check);

if best_valid_for_candidate.distance > best_valid.distance {
best_valid = best_valid_for_candidate;
Expand All @@ -80,7 +82,7 @@ pub fn optimize<T: Point>(route: &[T], break_at: f32, legs: usize) -> Option<Opt
// Calculate the cumulative distance when going from fix to fix. This places an upper limit on the
// distance achievable with n legs and is used to calculate a minimum index where a path needs to end
// to have the possibility to achieve a better result than distance
fn find_min_stop_idx(dist_matrix: &[Vec<f32>], distance: f32) -> usize {
fn find_minimum_stop(dist_matrix: &[Vec<f32>], distance: f32) -> usize {
let mut i = 0;
let mut sum = 0.0;
loop {
Expand Down
Loading

0 comments on commit 791137b

Please sign in to comment.