diff --git a/Cargo.lock b/Cargo.lock index c903725..a5bbd6e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -68,6 +68,7 @@ dependencies = [ "csv 1.0.7 (registry+https://github.com/rust-lang/crates.io-index)", "fnv 1.0.6 (registry+https://github.com/rust-lang/crates.io-index)", "humantime 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "permutohedron 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)", "rstar 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "serde 1.0.92 (registry+https://github.com/rust-lang/crates.io-index)", "serde_derive 1.0.92 (registry+https://github.com/rust-lang/crates.io-index)", @@ -144,6 +145,11 @@ name = "pdqselect" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" +[[package]] +name = "permutohedron" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" + [[package]] name = "proc-macro2" version = "0.4.30" @@ -320,6 +326,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum num-traits 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "6ba9a427cfca2be13aa6f6403b0b7e7368fe982bfa16fccc450ce74c46cd9b32" "checksum numtoa 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef" "checksum pdqselect 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4ec91767ecc0a0bbe558ce8c9da33c068066c57ecc8bb8477ef8c1ad3ef77c27" +"checksum permutohedron 0.2.4 (registry+https://github.com/rust-lang/crates.io-index)" = "b687ff7b5da449d39e418ad391e5e08da53ec334903ddbb921db208908fc372c" "checksum proc-macro2 0.4.30 (registry+https://github.com/rust-lang/crates.io-index)" = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759" "checksum quick-error 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "9274b940887ce9addde99c4eee6b5c44cc494b182b97e73dc8ffdcb3397fd3f0" "checksum quote 0.6.12 (registry+https://github.com/rust-lang/crates.io-index)" = "faf4799c5d274f3868a4aae320a0a182cbd2baee377b378f080e16a23e9d80db" diff --git a/Cargo.toml b/Cargo.toml index b8e7814..05abe0f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,9 +3,12 @@ name = "ed_lrr" version = "0.1.0" authors = ["Daniel Seiller "] edition = "2018" +repository = "https://gitlab.com/Earthnuker/ed_lrr.git" +license = "WTFPL" -# [profile.release] -# debug = true +[[bin]] +name = "ed_lrr" +path = "src/main.rs" [dependencies] csv = "1.0.7" @@ -15,3 +18,4 @@ rstar = "0.4.0" humantime = "1.2.0" fnv = "1.0.6" structopt = "0.2.16" +permutohedron = "0.2.4" diff --git a/README.md b/README.md index cd0a995..4107199 100644 --- a/README.md +++ b/README.md @@ -2,10 +2,10 @@ ## Usage: -1. run `download.sh` and `process.py` (you can hit Ctrl+C when it says "Building KD-Tree..") -1. edit source, destination and range in `src/main.rs` -1. (optional) `set RUSTFLAGS=-C target-cpu=native` (Windows) or `export RUSTFLAGS=-C target-cpu=native` (Linux) -1. `cargo run --release` +1. download `bodies.json` and `systemsWithCoordinates.json` from https://www.edsm.net/en/nightly-dumps/ and place them in the `dumps` folder +2. run `process.py` in the `dumps` folder +3. run `cargo install --git https://gitlab.com/Earthnuker/ed_lrr.git` +4. run `ed_lrr --help` diff --git a/src/main.rs b/src/main.rs index 49f659b..88843c6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,18 +1,21 @@ extern crate csv; extern crate serde; -#[macro_use] extern crate structopt; #[macro_use] extern crate serde_derive; extern crate fnv; extern crate humantime; +extern crate permutohedron; +use core::cmp::Ordering; use fnv::{FnvHashMap, FnvHashSet}; use humantime::format_duration; +use permutohedron::LexicalPermutation; use rstar::{PointDistance, RTree, RTreeObject, AABB}; use std::collections::VecDeque; use std::hash::{Hash, Hasher}; use std::io::Write; use std::path::PathBuf; +use std::str::FromStr; use std::time::Instant; use structopt::StructOpt; @@ -41,21 +44,73 @@ struct Point { y: f32, z: f32, } + +#[derive(Debug)] +enum Mode { + BFS, + Greedy, + AStar, +} + +impl FromStr for Mode { + type Err = String; + fn from_str(s: &str) -> Result { + match s { + "bfs" => Ok(Mode::BFS), + "greedy" => Ok(Mode::Greedy), + "astar" => Ok(Mode::AStar), + _ => Err("Invalid Mode".to_string()), + } + } +} + #[derive(Debug, StructOpt)] -#[structopt(name = "ed_lrr", about = "Elite: Dangerous Long-Range Router")] -/// Plots a route through multiple systems using breadth-first search (Currently needs a lot of RAM (8+GB), sorry) +#[structopt( + name = "ed_lrr", + about = "Elite: Dangerous Long-Range Router", + rename_all = "kebab-case" +)] +/// Plots a route through multiple systems using breadth-first search (Currently needs a lot of RAM (about 6GB), sorry) struct Opt { - #[structopt(short = "r", long = "range")] + #[structopt(short, long = "range")] /// Jump Range range: f32, #[structopt( parse(from_os_str), - short = "p", + short = "f", long = "path", default_value = "./stars.csv" )] /// Path to stars.csv + /// + /// Generate using process.py (https://gitlab.com/Earthnuker/ed_lrr/raw/master/dumps/process.py) file_path: PathBuf, + + #[structopt(short = "p", long = "permute", conflicts_with = "full_permute")] + /// Permute intermediate hops to find shortest route + permute: bool, + + #[structopt(short = "fp", long = "full_permute", conflicts_with = "permute")] + /// Permute all hops to find shortest route + full_permute: bool, + + #[structopt(short = "g", long = "factor")] + /// Greedyness factor for A-Star (0=BFS, inf=Greedy) + factor: Option, + + #[structopt( + short, + long = "mode", + raw(possible_values = "&[\"bfs\", \"greedy\",\"astar\"]") + )] + /// Search mode + /// + /** + - BFS is guaranteed to find the shortest route but very slow + - Greedy is a lot faster but will probably not find the shortest route + - A-Star is a good middle ground between speed and accuracy + */ + mode: Mode, /// Systems to route through systems: Vec, } @@ -67,6 +122,15 @@ fn dist(p1: &[f32; 3], p2: &[f32; 3]) -> f32 { return (dx * dx + dy * dy + dz * dz).sqrt(); } +fn fcmp(a: &f32, b: &f32) -> Ordering { + match (a, b) { + (x, y) if x.is_nan() && y.is_nan() => Ordering::Equal, + (x, _) if x.is_nan() => Ordering::Greater, + (_, y) if y.is_nan() => Ordering::Less, + (..) => a.partial_cmp(&b).unwrap(), + } +} + impl Point { pub fn dist2(&self, p: &[f32; 3]) -> f32 { let dx = self.x - p[0]; @@ -178,22 +242,7 @@ impl Router { return self.tree.nearest_neighbor(point).unwrap(); } fn points_in_sphere(&self, center: &[f32; 3], radius: f32) -> impl Iterator { - let center: [f32; 3] = *center; - return self - .tree - .locate_in_envelope(&AABB::from_corners( - [ - center[0] - radius * 1f32, - center[1] - radius * 1f32, - center[2] - radius * 1f32, - ], - [ - center[0] + radius * 1f32, - center[1] + radius * 1f32, - center[2] + radius * 1f32, - ], - )) - .filter(move |p| (p.dist2(¢er) < (radius * radius))); + return self.tree.locate_within_distance(*center, radius * radius); } fn mult(&self, id: i64) -> f32 { @@ -210,21 +259,82 @@ impl Router { return self.scoopable.contains(&sys.id); } - pub fn name_multiroute(&self, waypoints: &Vec, range: f32) -> Vec<(&System, &Point)> { + pub fn best_name_multiroute( + &self, + waypoints: &Vec, + range: f32, + full: bool, + mode: Mode, + factor: f32, + ) -> Vec<(&System, &Point)> { + let mut best_score: f32 = std::f32::MAX; + let mut waypoints = waypoints.clone(); + let mut best_permutation_waypoints = waypoints.clone(); + let first = waypoints.first().cloned(); + let last = waypoints.last().cloned(); + let t_start = Instant::now(); + println!("Finding best permutation of hops..."); + while waypoints.prev_permutation() {} + loop { + let c_first = waypoints.first().cloned(); + let c_last = waypoints.last().cloned(); + if full || ((c_first == first) && (c_last == last)) { + let mut total_d = 0.0; + for pair in waypoints.windows(2) { + match pair { + [src, dst] => { + let (src, dst) = (self.name_to_point(&src), self.name_to_point(&dst)); + total_d += src.distp2(dst); + } + _ => panic!("Invalid routing parameters!"), + } + } + if total_d < best_score { + best_score = total_d; + best_permutation_waypoints = waypoints.clone(); + } + } + if !waypoints.next_permutation() { + break; + } + } + + println!("Done in {}!", format_duration(t_start.elapsed())); + println!("Best permutation: {:?}", best_permutation_waypoints); + return self.name_multiroute(&best_permutation_waypoints, range, mode, factor); + } + + pub fn name_multiroute( + &self, + waypoints: &Vec, + range: f32, + mode: Mode, + factor: f32, + ) -> Vec<(&System, &Point)> { let mut coords = Vec::new(); for p in waypoints { let p = self.name_to_point(p); let s = [p.x, p.y, p.z]; coords.push(s); } - return self.multiroute(coords.as_slice(), range); + return self.multiroute(coords.as_slice(), range, mode, factor); } - pub fn multiroute(&self, waypoints: &[[f32; 3]], range: f32) -> Vec<(&System, &Point)> { + pub fn multiroute( + &self, + waypoints: &[[f32; 3]], + range: f32, + mode: Mode, + factor: f32, + ) -> Vec<(&System, &Point)> { let mut route = Vec::new(); for pair in waypoints.windows(2) { match pair { &[src, dst] => { - let block = self.route(&src, &dst, range); + let block = match mode { + Mode::BFS => self.route_bfs(&src, &dst, range), + Mode::Greedy => self.route_greedy(&src, &dst, range), + Mode::AStar => self.route_astar(&src, &dst, range, factor), + }; if route.is_empty() { route.extend(block.iter()); } else { @@ -237,32 +347,241 @@ impl Router { return route; } - fn sys_to_point(&self, id: i64) -> Option<&Point> { + fn sys_to_point(&self, id: i64) -> &Point { for p in &self.tree { if p.id == id { - return Some(p); + return p; } } - return None; + eprintln!("Sytem-ID not found: \"{}\"", id); + std::process::exit(1); } fn name_to_point(&self, name: &str) -> &Point { for sys in self.systems.values() { if sys.name == name { - return self.sys_to_point(sys.id).unwrap(); + return self.sys_to_point(sys.id); } } eprintln!("Sytem not found: \"{}\"", name); std::process::exit(1); } - pub fn route(&self, src: &[f32; 3], dst: &[f32; 3], range: f32) -> Vec<(&System, &Point)> { + pub fn route_astar( + &self, + src: &[f32; 3], + dst: &[f32; 3], + range: f32, + factor: f32, + ) -> Vec<(&System, &Point)> { + if factor == 0.0 { + return self.route_bfs(src, dst, range); + } + println!("Running A-Star with greedy factor of {}", factor); let start_sys = self.closest(src); let goal_sys = self.closest(dst); + let start_sys_name = self.systems.get(&start_sys.id).unwrap().name.clone(); + let goal_sys_name = self.systems.get(&goal_sys.id).unwrap().name.clone(); + { + let d = dist(src, dst); + println!( + "Plotting route from {} to {}...", + start_sys_name, goal_sys_name + ); + println!( + "Jump Range: {} Ly, Distance: {} Ly, Theoretical Jumps: {}", + range, + d, + d / range + ); + } + let total = self.tree.size() as f32; + let mut prev = FnvHashMap::default(); + let mut seen = FnvHashSet::default(); + let t_start = Instant::now(); + let mut found = false; + let mut maxd = 0; + let mut queue: Vec<(usize, usize, &Point)> = Vec::new(); + queue.push(( + 0, // depth + (start_sys.distp(goal_sys) / range) as usize, // h + &start_sys, + )); + seen.insert(start_sys.id); + + while !(queue.is_empty() || found) { + while let Some((depth, _, sys)) = queue.pop() { + if depth > maxd { + maxd = depth; + print!( + "[{}] Depth: {}, Queue: {}, Seen: {} ({:.02}%) \r", + format_duration(t_start.elapsed()), + depth, + queue.len(), + seen.len(), + ((seen.len() * 100) as f32) / total + ); + std::io::stdout().flush().unwrap(); + } + if sys.id == goal_sys.id { + found = true; + break; + } + queue.extend( + self.neighbours(&sys, range) + .filter(|&nb| (self.valid(nb) || (nb.id == goal_sys.id))) + .filter(|&nb| seen.insert(nb.id)) + .map(|nb| { + prev.insert(nb.id, sys.id); + let d_g = (nb.distp(goal_sys) / range) as usize; + return (depth + 1, d_g, nb); + }), + ); + queue.sort_by(|b, a| { + let (a_0, a_1) = (a.0 as f32, a.1 as f32); + let (b_0, b_1) = (b.0 as f32, b.1 as f32); + let v_a = a_0 + a_1 * factor; + let v_b = b_0 + b_1 * factor; + return fcmp(&v_a, &v_b); + }); + // queue.reverse(); + } + } + println!(); + + println!(); + if !found { + eprintln!( + "No route from {} to {} found!", + start_sys_name, goal_sys_name + ); + return Vec::new(); + } + let points = self.preload_points(); + let mut v: Vec<(&System, &Point)> = Vec::new(); + let mut prev_sys_id = goal_sys.id; + loop { + if let Some(sys) = self.systems.get(&prev_sys_id) { + v.push((sys, points[&sys.id])); + } else { + break; + }; + match prev.get(&prev_sys_id) { + Some(sys_id) => prev_sys_id = *sys_id, + None => { + break; + } + } + } + v.reverse(); + return v; + } + + pub fn route_greedy( + &self, + src: &[f32; 3], + dst: &[f32; 3], + range: f32, + ) -> Vec<(&System, &Point)> { + println!("Running Greedy-Search"); + let start_sys = self.closest(src); + let goal_sys = self.closest(dst); + let start_sys_name = self.systems.get(&start_sys.id).unwrap().name.clone(); + let goal_sys_name = self.systems.get(&goal_sys.id).unwrap().name.clone(); + { + let d = dist(src, dst); + println!( + "Plotting route from {} to {}...", + start_sys_name, goal_sys_name + ); + println!( + "Jump Range: {} Ly, Distance: {} Ly, Theoretical Jumps: {}", + range, + d, + d / range + ); + } + let total = self.tree.size() as f32; + let mut prev = FnvHashMap::default(); + let mut seen = FnvHashSet::default(); + let t_start = Instant::now(); + let mut found = false; + let mut maxd = 0; + let mut queue: Vec<(f32, f32, usize, &Point)> = Vec::new(); + queue.push(( + -self.mult(goal_sys.id), + start_sys.distp2(goal_sys), + 0, + &start_sys, + )); + seen.insert(start_sys.id); + while !(queue.is_empty() || found) { + std::io::stdout().flush().unwrap(); + while let Some((_, _, depth, sys)) = queue.pop() { + if depth > maxd { + maxd = depth; + print!( + "[{}] Depth: {}, Queue: {}, Seen: {} ({:.02}%) \r", + format_duration(t_start.elapsed()), + depth, + queue.len(), + seen.len(), + ((seen.len() * 100) as f32) / total + ); + } + if sys.id == goal_sys.id { + found = true; + break; + } + queue.extend( + self.neighbours(&sys, range) + .filter(|&nb| (self.valid(nb) || (nb.id == goal_sys.id))) + .filter(|&nb| seen.insert(nb.id)) + .map(|nb| { + prev.insert(nb.id, sys.id); + return (-self.mult(nb.id), nb.distp2(goal_sys), depth + 1, nb); + }), + ); + queue.sort_by(|a, b| fcmp(&a.0, &b.0).then(fcmp(&a.1, &b.1))); + queue.reverse(); + } + } + println!(); + if !found { + eprintln!( + "No route from {} to {} found!", + start_sys_name, goal_sys_name + ); + return Vec::new(); + } + let points = self.preload_points(); + let mut v: Vec<(&System, &Point)> = Vec::new(); + let mut prev_sys_id = goal_sys.id; + loop { + if let Some(sys) = self.systems.get(&prev_sys_id) { + v.push((sys, points[&sys.id])); + } else { + break; + }; + match prev.get(&prev_sys_id) { + Some(sys_id) => prev_sys_id = *sys_id, + None => { + break; + } + } + } + v.reverse(); + return v; + } + + pub fn route_bfs(&self, src: &[f32; 3], dst: &[f32; 3], range: f32) -> Vec<(&System, &Point)> { + println!("Running BFS"); + let start_sys = self.closest(src); + let goal_sys = self.closest(dst); + let start_sys_name = self.systems.get(&start_sys.id).unwrap().name.clone(); + let goal_sys_name = self.systems.get(&goal_sys.id).unwrap().name.clone(); { let d = dist(src, dst); - let start_sys_name = self.systems.get(&start_sys.id).unwrap().name.clone(); - let goal_sys_name = self.systems.get(&goal_sys.id).unwrap().name.clone(); println!( "Plotting route from {} to {}...", start_sys_name, goal_sys_name @@ -314,6 +633,10 @@ impl Router { } println!(); if !found { + eprintln!( + "No route from {} to {} found!", + start_sys_name, goal_sys_name + ); return Vec::new(); } let points = self.preload_points(); @@ -344,12 +667,31 @@ fn main() { let router: Router = Router::new(&path); println!("Done in {}!", format_duration(t_load.elapsed())); let t_route = Instant::now(); - let route = router.name_multiroute(&opts.systems, opts.range); + let route = if opts.permute || opts.full_permute { + router.best_name_multiroute( + &opts.systems, + opts.range, + opts.full_permute, + opts.mode, + opts.factor.unwrap_or(1.0), + ) + } else { + router.name_multiroute( + &opts.systems, + opts.range, + opts.mode, + opts.factor.unwrap_or(1.0), + ) + }; println!( "Done in {} ({} Jumps)!\n", format_duration(t_route.elapsed()), route.len(), ); + if route.len() == 0 { + eprintln!("No route found!"); + return; + } let mut total: f32 = 0.0; for ((sys1, p1), (_, p2)) in route.iter().zip(route.iter().skip(1)) { let dist = p1.distp(p2);