#![feature(binary_heap_retain)] #![allow(dead_code, clippy::needless_return, clippy::too_many_arguments)] #![warn(rust_2018_idioms, rust_2021_compatibility, clippy::disallowed_types)] //! # Elite: Danerous Long Range Router pub mod common; pub mod galaxy; pub mod journal; pub mod mmap_csv; #[cfg(feature = "profiling")] pub mod profiling; pub mod route; pub mod search_algos; pub mod ship; // ========================= use stats_alloc::{Region, StatsAlloc, INSTRUMENTED_SYSTEM}; use std::alloc::System as SystemAlloc; use std::collections::BTreeMap; use std::io::{BufWriter, Write}; use std::path::Path; use std::time::Instant; #[cfg(not(feature = "profiling"))] #[global_allocator] static GLOBAL: &StatsAlloc = &INSTRUMENTED_SYSTEM; // ========================= #[cfg(not(feature = "profiling"))] mod profiling { pub fn init() {} } use crate::common::{grid_stats, EdLrrError, SysEntry, System}; #[cfg(feature = "profiling")] use crate::profiling::*; use crate::route::{Router, SearchState}; use crate::ship::Ship; use eyre::Result; #[cfg(not(feature = "profiling"))] use log::*; use pyo3::create_exception; use pyo3::exceptions::*; use pyo3::prelude::*; use pyo3::types::{IntoPyDict, PyDict, PyTuple}; use route::PyModeConfig; use std::{collections::HashMap, convert::TryInto, fs::File, path::PathBuf}; #[cfg(feature = "mem_profiling")] #[global_allocator] static GLOBAL: ProfiledAllocator = ProfiledAllocator::new(std::alloc::System, 1024); create_exception!(_ed_lrr, RoutingError, PyException); create_exception!(_ed_lrr, ProcessingError, PyException); create_exception!(_ed_lrr, ResolveError, PyException); #[derive(Debug)] enum RangeOrShip { Range(f32), Ship(Ship), } impl FromPyObject<'_> for RangeOrShip { fn extract(ob: &PyAny) -> PyResult { if let Ok(n) = ob.extract() { return Ok(Self::Range(n)); } let s: PyShip = ob.extract()?; return Ok(Self::Ship(s.ship)); } } #[pyclass(dict)] #[derive(Debug)] #[pyo3(text_signature = "(callback, /)")] struct PyRouter { router: Router, stars_path: Option, } impl PyRouter { fn check_stars(&self) -> PyResult { self.stars_path .as_ref() .ok_or_else(|| PyErr::from(EdLrrError::RuntimeError("no stars.csv loaded".to_owned()))) .map(PathBuf::from) } } #[pymethods] impl PyRouter { #[new] #[args(callback = "None")] fn new(callback: Option) -> Self { let mut router = Router::new(); if callback.is_some() { router.set_callback(Box::new(move |state: &SearchState| { let gil_guard = Python::acquire_gil(); let py = gil_guard.python(); match callback.as_ref() { Some(cb) => cb.call(py, (state.clone(),), None), None => Ok(py.None()), } })) } PyRouter { router, stars_path: None, } } #[args(primary_only = "false", immediate = "false")] #[pyo3(text_signature = "(path, primary_only, /)")] fn load(&mut self, path: String, py: Python<'_>, immediate: bool) -> PyResult { self.stars_path = Some(path); if immediate { self.router .load(&self.check_stars()?) .map_err(PyErr::new::)?; } Ok(py.None()) } #[pyo3(text_signature = "(/)")] fn unload(&mut self, py: Python<'_>) -> PyObject { self.router.unload(); py.None() } fn plot(&mut self, py: Python<'_>) -> PyResult { let stars_path = self.check_stars()?; let route_res = self.router.load(&stars_path); if let Err(err_msg) = route_res { return Err(PyErr::new::(err_msg)); }; let mut max_v = [0f32, 0f32, 0f32]; let mut min_v = [0f32, 0f32, 0f32]; for node in self.router.get_tree().iter() { for i in 0..3 { if node.pos[i] > max_v[i] { max_v[i] = node.pos[i]; } if node.pos[i] < min_v[i] { min_v[i] = node.pos[i]; } } } let plot_bbox: ((f32, f32), (f32, f32)) = ((min_v[0], max_v[0]), (min_v[2], max_v[2])); Ok(plot_bbox.to_object(py)) } fn run_bfs(&mut self, range: f32, py: Python<'_>) -> PyResult { let stars_path = self.check_stars()?; let route_res = self.router.load(&stars_path); if let Err(err_msg) = route_res { return Err(PyErr::new::(err_msg)); }; self.router .precomp_bfs(range) .map_err(PyErr::new::) .map(|_| py.None()) } fn precompute_graph(&mut self, range: f32, py: Python<'_>) -> PyResult { let stars_path = self.check_stars()?; let route_res = self.router.load(&stars_path); if let Err(err_msg) = route_res { return Err(PyErr::new::(err_msg)); }; self.router .precompute_graph(range) .map_err(PyErr::new::) .map(|_| py.None()) } fn nb_perf_test(&mut self, range: f32, py: Python<'_>) -> PyResult { let stars_path = self.check_stars()?; let route_res = self.router.load(&stars_path); if let Err(err_msg) = route_res { return Err(PyErr::new::(err_msg)); }; let tree = self.router.get_tree(); let total_nodes = tree.size(); let mut total_nbs = 0; for (n, node) in tree.iter().enumerate() { total_nbs += self.router.neighbours(node, range).count(); // nbmap.insert(node.id, nbs); if n % 100_000 == 0 { let avg = total_nbs as f64 / (n + 1) as f64; info!("{}/{} {} ({})", n, total_nodes, total_nbs, avg); } } let avg = total_nbs as f64 / total_nodes as f64; info!("Total: {} ({})", total_nbs, avg); Ok(py.None()) } fn precompute_neighbors(&mut self, range: f32, py: Python<'_>) -> PyResult { let stars_path = self.check_stars()?; let route_res = self.router.load(&stars_path); if let Err(err_msg) = route_res { return Err(PyErr::new::(err_msg)); }; self.router .precompute_all(range) .map_err(PyErr::new::) .map(|_| py.None()) } fn bfs_test(&mut self, range: f32) -> PyResult<()> { use rand::prelude::*; let stars_path = self.check_stars()?; let route_res = self.router.load(&stars_path); if let Err(err_msg) = route_res { return Err(PyErr::new::(err_msg)); }; let mut rng = rand::rngs::StdRng::seed_from_u64(0); let nodes = self.router.get_tree().size(); loop { let source = *self .router .get_tree() .iter() .nth(rng.gen_range(0..nodes)) .unwrap(); let goal = *self .router .get_tree() .iter() .nth(rng.gen_range(0..nodes)) .unwrap(); self.router.bfs_loop_test(range, &source, &goal, 0); for w in 0..=15 { self.router.bfs_loop_test(range, &source, &goal, 2usize.pow(w)); } } Ok(()) } #[args( greedyness = "0.5", max_dist = "0.0", num_workers = "0", beam_width = "BeamWidth::Absolute(0)" )] #[pyo3(text_signature = "(hops, range, mode, num_workers, /)")] fn route( &mut self, hops: Vec, range: RangeOrShip, mode: Option, num_workers: usize, ) -> PyResult> { let stars_path = self.check_stars()?; let route_res = self.router.load(&stars_path); if let Err(err_msg) = route_res { return Err(PyErr::new::(err_msg)); }; info!("Resolving systems..."); let ids: Vec = match resolve(&hops, &self.router.path, true) { Ok(sytems) => sytems.into_iter().map(|id| id.into_id()).collect(), Err(err_msg) => { return Err(err_msg.into()); } }; let mut is_default = false; let mut is_ship = false; info!("{:?}", mode); let mut mode = match mode { Some(mode) => mode, None => { let mode = PyModeConfig::default(); is_default = true; mode } }; if mode.mode.is_empty() { if mode.ship.is_none() { mode.mode = "bfs".to_string(); } else { mode.mode = "ship".to_string(); if mode.ship_mode == *"" { mode.ship_mode = "jumps".to_string(); } } } let range = match range { RangeOrShip::Range(r) => Some(r), RangeOrShip::Ship(ship) => { mode.mode = "ship".into(); mode.ship = Some(ship); is_ship = true; None } }; info!("{:?}", mode); let mode = mode.try_into()?; if is_default && !is_ship { warn!("no mode specified, defaulting to {}", mode); } #[cfg(not(feature = "profiling"))] let reg = Region::new(GLOBAL); let res = match self.router.compute_route(&ids, range, mode, num_workers) { Ok(route) => Ok(route), Err(err_msg) => Err(PyErr::new::(err_msg)), }; #[cfg(not(feature = "profiling"))] println!("{:?}", reg.change()); return res; } fn perf_test(&self, callback: PyObject, py: Python<'_>) -> PyResult { use common::TreeNode; let node = TreeNode { pos: [-65.21875, 7.75, -111.03125], flags: 1, id: 0, }; let goal = TreeNode { pos: [-9530.5, -910.28125, 19808.125], flags: 1, id: 1, }; let kwargs = vec![("goal", goal), ("node", node)].into_py_dict(py); let mut n: usize = 0; let mut d: f64 = 0.0; let num_loops = 10_000_000; loop { let pool = unsafe { Python::new_pool(py) }; let t_start = std::time::Instant::now(); for _ in 0..num_loops { let val: f64 = callback.call(py, (), Some(kwargs))?.extract(py)?; } d += t_start.elapsed().as_secs_f64(); drop(pool); n += num_loops; let dt = std::time::Duration::from_secs_f64(d / (n as f64)); println!("{}: {:?}", n, dt); } Ok(py.None()) } #[args(grid_size = "1.0")] #[pyo3(text_signature = "(grid_size)")] fn get_grid(&self, grid_size: f32, py: Python<'_>) -> PyResult { let stars_path = self.check_stars()?; grid_stats(&stars_path, grid_size) .map(|ret| ret.to_object(py)) .map_err(PyErr::new::) } fn floyd_warshall_test(&mut self, range: f32) -> PyResult> { let stars_path = self.check_stars()?; self.router .load(&stars_path) .map_err(PyErr::new::)?; let res = self .router .floyd_warshall(range) .map_err(PyErr::new::)?; Ok(res) } #[args(hops = "*")] #[pyo3(text_signature = "(sys_1, sys_2, ..., /)")] fn resolve(&self, hops: Vec, py: Python<'_>) -> PyResult { info!("Resolving systems..."); let stars_path = self.check_stars()?; let systems: Vec = match resolve(&hops, &stars_path, false) { Ok(sytems) => sytems.into_iter().map(|sys| sys.into_system()).collect(), Err(err_msg) => { return Err(err_msg.into()); } }; let ret: Vec<(_, System)> = hops .into_iter() .zip(systems.iter()) .map(|(id, sys)| (id, sys.clone())) .collect(); Ok(PyDict::from_sequence(py, ret.to_object(py))?.to_object(py)) } fn str_tree_test(&self) -> common::EdLrrResult<()> { use common::BKTree; const CHUNK_SIZE: usize = 4_000_000; let path = self.check_stars()?; let reader: csv::Reader = csv::ReaderBuilder::new() .has_headers(false) .from_path(path) .map_err(EdLrrError::from)?; let mut data: Vec = Vec::with_capacity(CHUNK_SIZE); let t_start = Instant::now(); let mut base_id = 0; let mut wr = BufWriter::new(File::create("test.bktree")?); for sys in reader.into_deserialize::() { let sys = sys?; data.push(sys.name); if data.len() > CHUNK_SIZE { let tree = BKTree::new(&data, base_id); tree.dump(&mut wr)?; base_id = sys.id; } } if !data.is_empty() { let tree = BKTree::new(&data, base_id); tree.dump(&mut wr)?; } wr.flush()?; println!("Took: {:?}", t_start.elapsed()); Ok(()) } fn __str__(&self) -> PyResult { Ok(format!("{:?}", &self)) } fn __repr__(&self) -> PyResult { Ok(format!("{:?}", &self)) } } enum ResolveResult { System(System), ID(u32), } impl ResolveResult { fn into_id(self) -> u32 { match self { Self::System(sys) => sys.id, Self::ID(id) => id, } } fn into_system(self) -> System { if let Self::System(sys) = self { return sys; } panic!("Tried to unwrap ID into System"); } } fn resolve( entries: &[SysEntry], path: &Path, id_only: bool, ) -> Result, EdLrrError> { let mut names: Vec = Vec::new(); let mut ret: Vec = Vec::new(); let mut needs_rtree = false; for ent in entries { match ent { SysEntry::Name(name) => names.push(name.to_owned()), SysEntry::Pos(_) => { needs_rtree |= true; } _ => (), } } if !path.exists() { return Err(EdLrrError::ResolveError(format!( "Source file {:?} does not exist!", path.display() ))); } let name_ids = if !names.is_empty() { mmap_csv::mmap_csv(path, names)? } else { HashMap::new() }; let tmp_r = needs_rtree .then(|| { let mut r = Router::new(); r.load(path).map(|_| r) }) .transpose()?; for ent in entries { match ent { SysEntry::Name(name) => { let ent_res = name_ids.get(name).ok_or_else(|| { EdLrrError::ResolveError(format!("System {} not found", name)) })?; let sys = ent_res.as_ref().ok_or_else(|| { EdLrrError::ResolveError(format!("System {} not found", name)) })?; ret.push(*sys); } SysEntry::ID(id) => ret.push(*id), SysEntry::Pos((x, y, z)) => ret.push( tmp_r .as_ref() .unwrap() .closest(&[*x, *y, *z]) .ok_or_else(|| EdLrrError::ResolveError("No systems loaded!".to_string()))? .id, ), } } if id_only { return Ok(ret.iter().map(|id| ResolveResult::ID(*id)).collect()); } else { let mut lc = route::LineCache::create(path)?; let mut systems = vec![]; for id in ret { let sys = ResolveResult::System(lc.get(id)?.unwrap()); systems.push(sys) } return Ok(systems); } } #[pyclass(dict)] #[derive(Debug, Clone)] struct PyShip { ship: Ship, } #[pymethods] impl PyShip { #[staticmethod] fn from_loadout(py: Python<'_>, loadout: &str) -> PyResult { match Ship::new_from_json(loadout) { Ok(ship) => Ok((PyShip { ship: ship.1 }).into_py(py)), Err(err_msg) => Err(PyErr::new::(err_msg)), } } #[staticmethod] fn from_journal(py: Python<'_>) -> PyResult { let mut ship = match Ship::new_from_journal() { Ok(ship) => ship, Err(err_msg) => { return Err(PyErr::new::(err_msg)); } }; let ships: Vec<(PyObject, PyObject)> = ship .drain() .map(|(ship_name, ship)| { let ship_name_py = ship_name.to_object(py); let ship_py = (PyShip { ship }).into_py(py); (ship_name_py, ship_py) }) .collect(); Ok(PyDict::from_sequence(py, ships.to_object(py))?.to_object(py)) } fn to_dict(&self, py: Python<'_>) -> PyResult { self.ship.to_object(py) } #[pyo3(text_signature = "(dist, /)")] fn fuel_cost(&self, _py: Python<'_>, dist: f32) -> f32 { self.ship.fuel_cost(dist) } #[getter] fn range(&self, _py: Python<'_>) -> f32 { self.ship.range() } #[getter] fn max_range(&self, _py: Python<'_>) -> f32 { self.ship.max_range() } #[pyo3(text_signature = "(dist, /)")] fn make_jump(&mut self, dist: f32, _py: Python<'_>) -> Option { self.ship.make_jump(dist) } #[pyo3(text_signature = "(dist, /)")] fn can_jump(&self, dist: f32, _py: Python<'_>) -> bool { self.ship.can_jump(dist) } #[args(fuel_amount = "None")] #[pyo3(text_signature = "(fuel_amount, /)")] fn refuel(&mut self, fuel_amount: Option, _py: Python<'_>) { if let Some(fuel) = fuel_amount { self.ship.fuel_mass = (self.ship.fuel_mass + fuel).min(self.ship.fuel_capacity) } else { self.ship.fuel_mass = self.ship.fuel_capacity; } } #[pyo3(text_signature = "(factor, /)")] fn boost(&mut self, factor: f32, _py: Python<'_>) { self.ship.boost(factor); } fn __str__(&self) -> PyResult { Ok(format!("{:?}", &self.ship)) } fn __repr__(&self) -> PyResult { Ok(format!("{:?}", &self.ship)) } } impl PyShip { fn get_ship(&self) -> Ship { self.ship.clone() } } #[pyfunction] fn preprocess_edsm( _bodies_path: &str, _systems_path: &str, _out_path: &str, _py: Python<'_>, ) -> PyResult<()> { Err(pyo3::exceptions::PyNotImplementedError::new_err( "please use Spansh's Galaxy dump and preprocess_galaxy()", )) } fn to_py_value(value: eval::Value, py: Python<'_>) -> PyResult { type Value = eval::Value; match value { Value::String(s) => Ok(s.to_object(py)), Value::Number(n) => { if let Some(n) = n.as_u64() { return Ok(n.to_object(py)); } if let Some(n) = n.as_i64() { return Ok(n.to_object(py)); } return Ok(n.as_f64().unwrap().to_object(py)); } Value::Bool(b) => Ok(b.to_object(py)), Value::Array(mut t) => { let mut res: Vec = vec![]; for v in t.drain(..) { res.push(to_py_value(v, py)?); } Ok(PyTuple::new(py, &res).to_object(py)) } Value::Object(o) => { let res = PyDict::new(py); for (k, v) in o.iter() { res.set_item(k, to_py_value(v.clone(), py)?)?; } Ok(res.to_object(py)) } Value::Null => Ok(py.None()), } } fn to_py(res: Result, py: Python<'_>) -> PyResult { res.map_err(|e| PyErr::from(EdLrrError::EvalError(e))) .and_then(|r| to_py_value(r, py)) } #[pyfunction] #[pyo3(text_signature = "(expr)")] fn expr_test(expr: &str, py: Python<'_>) -> PyResult { use eval::{to_value, Expr, Value}; let mut res = Expr::new(expr) .compile() .map_err(|e| PyErr::from(EdLrrError::EvalError(e)))?; let mut hm: HashMap<&str, Value> = HashMap::new(); hm.insert("foo", to_value(42)); hm.insert("bar", to_value((-1, -2, -3))); res = res.value("x", vec!["Hello", "world", "!"]); res = res.value("y", 42); res = res.value("p", (2.17, 5.14, 1.62)); res = res.value("hw", "Hello World!"); res = res.value("hm", hm); to_py(res.exec(), py) } #[pyfunction] #[pyo3(text_signature = "(path, out_path, /)")] fn preprocess_galaxy(path: &str, out_path: &str) -> PyResult<()> { use common::build_index; use galaxy::process_galaxy_dump; let path = PathBuf::from(path); let out_path = PathBuf::from(out_path); process_galaxy_dump(&path, &out_path).unwrap(); build_index(&out_path)?; Ok(()) } #[pymodule] pub fn _ed_lrr(_py: Python<'_>, m: &PyModule) -> PyResult<()> { better_panic::install(); pyo3_log::init(); profiling::init(); m.add_class::()?; m.add_class::()?; m.add_wrapped(pyo3::wrap_pyfunction!(preprocess_galaxy))?; m.add_wrapped(pyo3::wrap_pyfunction!(preprocess_edsm))?; m.add_wrapped(pyo3::wrap_pyfunction!(expr_test))?; Ok(()) }