477 lines
13 KiB
Rust
477 lines
13 KiB
Rust
|
|
use std::{io::Cursor, cmp::{Ordering, Reverse}, collections::{VecDeque, BinaryHeap, HashMap}, hash::Hash};
|
|
use std::collections::{BTreeMap, BTreeSet};
|
|
|
|
use edalyze::bindata::*;
|
|
use rstar::primitives::{PointWithData, GeomWithData};
|
|
|
|
#[derive(Default)]
|
|
struct PrevLink {
|
|
system: usize,
|
|
distance: f32,
|
|
uses_jumponium: bool,
|
|
}
|
|
|
|
struct IndexData {
|
|
cost: f32,
|
|
reachable: bool,
|
|
last: PrevLink,
|
|
sys_data: System,
|
|
}
|
|
|
|
impl From<System> for IndexData {
|
|
fn from(sys: System) -> Self {
|
|
Self {
|
|
cost: f32::INFINITY,
|
|
reachable: false,
|
|
last: PrevLink::default(),
|
|
sys_data: sys,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Copy, Clone)]
|
|
struct SearchNode {
|
|
id: usize,
|
|
cost: f32,
|
|
heuristic: f32,
|
|
last: (usize, bool)
|
|
}
|
|
|
|
trait SearchState {
|
|
type Id: Ord+Hash+Copy;
|
|
fn cost(&self) -> f32;
|
|
fn heuristic(&self) -> f32;
|
|
fn id(&self) -> Self::Id;
|
|
}
|
|
|
|
// ordering tuned for BinaryHeap
|
|
impl Ord for SearchNode {
|
|
fn cmp(&self, other: &Self) -> Ordering {
|
|
f32::total_cmp(&(self.cost + self.heuristic), &(other.cost + other.heuristic))
|
|
}
|
|
}
|
|
|
|
impl PartialOrd for SearchNode {
|
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
|
Some(self.cmp(other))
|
|
}
|
|
}
|
|
|
|
impl PartialEq for SearchNode {
|
|
fn eq(&self, other: &Self) -> bool {
|
|
(self.cost + self.heuristic) == (other.cost + other.heuristic)
|
|
}
|
|
}
|
|
impl Eq for SearchNode {}
|
|
|
|
impl SearchState for SearchNode {
|
|
type Id = usize;
|
|
|
|
fn cost(&self) -> f32 {
|
|
self.cost
|
|
}
|
|
|
|
fn heuristic(&self) -> f32 {
|
|
self.heuristic
|
|
}
|
|
|
|
fn id(&self) -> Self::Id {
|
|
self.id
|
|
}
|
|
}
|
|
|
|
trait SearchQueue<T> {
|
|
fn q_next(&mut self) -> Option<T>;
|
|
fn q_empty(&self) -> bool;
|
|
fn q_push(&mut self, item: T);
|
|
fn q_clear(&mut self);
|
|
fn q_len(&self) -> usize;
|
|
}
|
|
|
|
struct SearchEnv<Q> {
|
|
rst: rstar::RTree<GeomWithData<Point, usize>>,
|
|
meta: Vec<IndexData>,
|
|
base_jump: f32,
|
|
init_system: usize,
|
|
target_system: usize,
|
|
jumponium_cost_factor: f32,
|
|
queue: Q,
|
|
visits: usize,
|
|
}
|
|
|
|
impl<Q: SearchQueue<SearchNode>> SearchEnv<Q> {
|
|
fn reset(&mut self) {
|
|
for item in self.meta.iter_mut() {
|
|
item.cost = f32::INFINITY;
|
|
item.reachable = false;
|
|
}
|
|
self.queue.q_clear();
|
|
let init_node = SearchNode{
|
|
id: self.init_system,
|
|
cost: 0.,
|
|
heuristic: self.heuristic(self.init_system),
|
|
last: (self.init_system, false),
|
|
};
|
|
self.queue.q_push(init_node);
|
|
}
|
|
|
|
fn is_done(&self) -> bool {
|
|
self.queue.q_empty() || self.meta[self.target_system].reachable
|
|
}
|
|
fn search(&mut self) -> Option<f32> {
|
|
self.reset();
|
|
eprintln!("");
|
|
while !self.is_done() {
|
|
let node = self.queue.q_next().unwrap();
|
|
// check whether node should be re-expanded
|
|
{
|
|
let scoord = self.meta[node.last.0].sys_data.coords;
|
|
let n = &mut self.meta[node.id];
|
|
if n.reachable && n.cost < node.cost {
|
|
continue;
|
|
}
|
|
n.cost = node.cost;
|
|
n.reachable = true;
|
|
n.last = PrevLink {
|
|
uses_jumponium: node.last.1,
|
|
system: node.last.0,
|
|
distance: n.sys_data.coords.distance(scoord),
|
|
};
|
|
}
|
|
self.visits += 1;
|
|
if self.visits % 1000 == 0 {
|
|
eprintln!("\x1b[1A\x1b[K{}k ({} in queue)", self.visits/100, self.queue.q_len());
|
|
}
|
|
self.visit_star(node.id, node.cost);
|
|
}
|
|
|
|
let tsys = &self.meta[self.target_system];
|
|
tsys.reachable.then_some(tsys.cost)
|
|
}
|
|
|
|
fn heuristic(&self, sys_id: usize) -> f32 {
|
|
let tcoord = self.meta[self.target_system].sys_data.coords;
|
|
let scoord = self.meta[sys_id].sys_data.coords;
|
|
let dist = tcoord.distance(scoord);
|
|
(dist - self.jdist(sys_id)) / (4. * self.base_jump)
|
|
}
|
|
|
|
fn visit_star(&mut self, sys_id: usize, base_cost: f32) {
|
|
let star_jump = self.jdist(sys_id);
|
|
let star_jump_sq = star_jump * star_jump;
|
|
let base_jump_sq = self.base_jump * self.base_jump;
|
|
let augm_jump_sq = base_jump_sq * 4.;
|
|
|
|
let max_jump_sq = augm_jump_sq.max(star_jump_sq);
|
|
//eprintln!("Max jump: {max_jump_sq}");
|
|
|
|
let cur_pos = self.meta[sys_id].sys_data.coords;
|
|
for n_star in self.rst.locate_within_distance(cur_pos, max_jump_sq) {
|
|
//eprintln!("Examining {}", self.meta[n_star.data].sys_data.name);
|
|
let dist_sq = n_star.geom().distance_sq(cur_pos);
|
|
let (cost_factor, _range, uses_jumponium) = if dist_sq > max_jump_sq {
|
|
continue;
|
|
} else if dist_sq < star_jump_sq {
|
|
(1., star_jump, false)
|
|
} else {
|
|
// must be a jumponium jump
|
|
(self.jumponium_cost_factor, 2. * self.base_jump, true)
|
|
};
|
|
|
|
// TODO: make an estimate of the fuel used
|
|
let cost = base_cost + cost_factor;
|
|
let heuristic = self.heuristic(n_star.data);
|
|
self.queue.q_push(SearchNode { id: n_star.data, cost, heuristic, last: (sys_id, uses_jumponium) });
|
|
}
|
|
}
|
|
|
|
fn jdist(&self, sys_id: usize) -> f32 {
|
|
self.base_jump * self.meta[sys_id].sys_data.jump_scale()
|
|
}
|
|
}
|
|
|
|
/// Unique priority queue
|
|
/// This contains a list of items, each with an ID and a weight. Only the minimum weight for a given item is kept.
|
|
struct UniquePQueue<Id, Weight, Data> {
|
|
node_data: HashMap<Id, (Weight, Data)>,
|
|
queue: BTreeSet<(Weight, Id)>,
|
|
}
|
|
impl<I,W,D> UniquePQueue<I,W,D>
|
|
where I: Hash+Ord+Copy,
|
|
W: Ord+Copy,
|
|
D: Clone {
|
|
pub fn insert(&mut self, id: I, weight: W, data: D) {
|
|
if let Some((o_weight, o_data)) = self.node_data.get_mut(&id) {
|
|
if *o_weight < weight {
|
|
return;
|
|
}
|
|
self.queue.remove(&(*o_weight, id));
|
|
*o_weight = weight;
|
|
*o_data = data;
|
|
} else {
|
|
self.node_data.insert(id, (weight, data));
|
|
}
|
|
|
|
self.queue.insert((weight, id));
|
|
}
|
|
|
|
pub fn clear(&mut self) {
|
|
self.node_data.clear();
|
|
self.queue.clear();
|
|
}
|
|
|
|
pub fn is_empty(&self) -> bool {
|
|
self.queue.is_empty()
|
|
}
|
|
|
|
pub fn len(&self) -> usize {
|
|
self.queue.len()
|
|
}
|
|
pub fn pop_next(&mut self) -> Option<D> {
|
|
self.queue.pop_first().map(|(_,d)| self.node_data[&d].1.clone())
|
|
}
|
|
|
|
pub fn new() -> Self {
|
|
Self {
|
|
node_data: HashMap::new(),
|
|
queue: BTreeSet::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Copy, Clone)]
|
|
struct TOf32(f32);
|
|
|
|
impl Ord for TOf32 {
|
|
fn cmp(&self, other: &Self) -> Ordering {
|
|
self.0.total_cmp(&other.0)
|
|
}
|
|
}
|
|
impl PartialOrd for TOf32 {
|
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
|
Some(self.cmp(other))
|
|
}
|
|
}
|
|
|
|
impl PartialEq for TOf32 {
|
|
fn eq(&self, other: &Self) -> bool {
|
|
self.cmp(other) == Ordering::Equal
|
|
}
|
|
}
|
|
|
|
impl Eq for TOf32 {}
|
|
|
|
// BFS
|
|
struct BFS<T>(VecDeque<T>);
|
|
impl<T> SearchQueue<T> for BFS<T> {
|
|
fn q_next(&mut self) -> Option<T> { self.0.pop_front() }
|
|
fn q_empty(&self) -> bool { self.0.is_empty() }
|
|
fn q_push(&mut self, item: T) { self.0.push_back(item); }
|
|
fn q_clear(&mut self) { self.0.clear(); }
|
|
fn q_len(&self) -> usize { self.0.len() }
|
|
}
|
|
impl<T> Default for BFS<T> {
|
|
fn default() -> Self { BFS(VecDeque::new()) }
|
|
}
|
|
|
|
struct CostCompare<T: SearchState>(T);
|
|
impl<T: SearchState> Ord for CostCompare<T> {
|
|
fn cmp(&self, other: &Self) -> Ordering {
|
|
f32::total_cmp(&self.0.cost(), &other.0.cost()).reverse()
|
|
}
|
|
}
|
|
impl<T: SearchState> PartialOrd for CostCompare<T> {
|
|
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
|
|
Some(self.cmp(other))
|
|
}
|
|
}
|
|
impl<T: SearchState> Eq for CostCompare<T> {
|
|
}
|
|
impl<T: SearchState> PartialEq for CostCompare<T> {
|
|
fn eq(&self, other: &Self) -> bool {
|
|
self.cmp(other) == Ordering::Equal
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
struct BFS1<T: SearchState>{
|
|
q: BinaryHeap<CostCompare<T>>,
|
|
seen: HashMap<T::Id, f32>,
|
|
}
|
|
|
|
impl<T: SearchState> SearchQueue<T> for BFS1<T> {
|
|
fn q_next(&mut self) -> Option<T> { self.q.pop().map(|x| x.0) }
|
|
fn q_empty(&self) -> bool { self.q.is_empty() }
|
|
fn q_push(&mut self, item: T) {
|
|
let id = item.id();
|
|
if let Some(odist) = self.seen.get(&id) {
|
|
if *odist < item.cost() {
|
|
return;
|
|
}
|
|
}
|
|
self.seen.insert(id, item.cost());
|
|
self.q.push(CostCompare(item));
|
|
}
|
|
fn q_clear(&mut self) {
|
|
self.q.clear();
|
|
self.seen.clear();
|
|
}
|
|
fn q_len(&self) -> usize { self.q.len() }
|
|
}
|
|
impl<T: SearchState> Default for BFS1<T> {
|
|
fn default() -> Self {
|
|
BFS1{
|
|
q: BinaryHeap::new(),
|
|
seen: HashMap::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
struct DFS<T>(Vec<T>);
|
|
impl<T> SearchQueue<T> for DFS<T> {
|
|
fn q_next(&mut self) -> Option<T> { self.0.pop() }
|
|
fn q_empty(&self) -> bool { self.0.is_empty() }
|
|
fn q_push(&mut self, item: T) { self.0.push(item); }
|
|
fn q_clear(&mut self) { self.0.clear(); }
|
|
fn q_len(&self) -> usize { self.0.len() }
|
|
}
|
|
impl<T> Default for DFS<T> {
|
|
fn default() -> Self { DFS(Vec::new()) }
|
|
}
|
|
|
|
struct AStar<T: SearchState>{
|
|
q: UniquePQueue<T::Id, TOf32, T>,
|
|
}
|
|
impl<T: SearchState + Clone> SearchQueue<T> for AStar<T>{
|
|
fn q_next(&mut self) -> Option<T> { self.q.pop_next() }
|
|
fn q_empty(&self) -> bool { self.q.is_empty() }
|
|
fn q_push(&mut self, item: T) {
|
|
self.q.insert(item.id(), TOf32(item.cost() + item.heuristic()), item);
|
|
}
|
|
fn q_clear(&mut self) {
|
|
self.q.clear();
|
|
}
|
|
fn q_len(&self) -> usize { self.q.len() }
|
|
}
|
|
impl<T: SearchState+Clone> Default for AStar<T> {
|
|
fn default() -> Self {
|
|
Self {
|
|
q: UniquePQueue::new(),
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
|
|
use structopt::StructOpt;
|
|
|
|
#[derive(StructOpt)]
|
|
struct Options {
|
|
#[structopt(short="r", default_value="10")]
|
|
jump_range: f32,
|
|
#[structopt(short="j", default_value="10")]
|
|
cost_factor: f32,
|
|
#[structopt(short="f", default_value="2MASS J07523444-2626443")]
|
|
start: String,
|
|
#[structopt(short="t", default_value="Angosk OM-W d1-0")]
|
|
end: String,
|
|
|
|
}
|
|
|
|
fn main() -> anyhow::Result<()> {
|
|
let opts = Options::from_args();
|
|
|
|
let mut reg_dat = Cursor::new(std::fs::read("region.dat")?);
|
|
let mut reg_nam = Cursor::new(std::fs::read("region.nam")?);
|
|
|
|
let mut systems = Vec::with_capacity(reg_dat.get_ref().len() / 16);
|
|
while let Ok(mut system) = System::read_from(&mut reg_dat) {
|
|
system.read_name(&mut reg_nam).ok();
|
|
systems.push(IndexData::from(system));
|
|
}
|
|
|
|
|
|
let rst = rstar::RTree::bulk_load(
|
|
systems.iter()
|
|
.enumerate()
|
|
.map(|(i,md)| GeomWithData::new(md.sys_data.coords, i))
|
|
.collect()
|
|
);
|
|
|
|
eprintln!("Done reading {} systems", systems.len());
|
|
let init_system = systems.iter().enumerate()
|
|
.filter(|(_,sys)| sys.sys_data.name == opts.start)
|
|
// .filter(|(_,sys)| sys.sys_data.name == "Haffner 18 LSS 27")
|
|
.map(|item| {
|
|
eprintln!("Start: {:?}", item.1.sys_data.coords);
|
|
item
|
|
})
|
|
.next()
|
|
.expect("init system should exist")
|
|
.0;
|
|
let target_system = systems.iter().enumerate()
|
|
.filter(|(_,sys)| sys.sys_data.name == opts.end)
|
|
.map(|item| {
|
|
eprintln!("End: {:?}", item.1.sys_data.coords);
|
|
item
|
|
})
|
|
.next()
|
|
.expect("init system should exist")
|
|
.0;
|
|
|
|
let mut env = SearchEnv {
|
|
rst,
|
|
meta: systems,
|
|
base_jump: opts.jump_range,
|
|
init_system,
|
|
target_system,
|
|
queue: AStar::default(),
|
|
jumponium_cost_factor: opts.cost_factor,
|
|
visits: 0,
|
|
};
|
|
if let Some(cost) = env.search() {
|
|
eprintln!("Reached in {cost}");
|
|
|
|
// produce a trace...
|
|
let mut cur_id = env.target_system;
|
|
let mut stack = vec![];
|
|
let mut total_dist = 0.;
|
|
while cur_id != env.init_system {
|
|
let sys = &env.meta[cur_id];
|
|
stack.push((&sys.sys_data, sys.last.distance, sys.last.uses_jumponium));
|
|
total_dist += sys.last.distance;
|
|
cur_id = sys.last.system;
|
|
}
|
|
|
|
|
|
let mut jcount = 0;
|
|
for (sys_data, dist, usej) in stack.iter().rev().copied() {
|
|
let jflag = if usej { "j" } else { "-" };
|
|
let name = &sys_data.name;
|
|
let fflag = if sys_data.star_flags & IS_FUEL != 0 { "f" } else { "-" };
|
|
println!(" {name:-40}: {fflag}{jflag} {dist:3.0}", );
|
|
if usej {
|
|
jcount += 1;
|
|
}
|
|
}
|
|
let opt_dist = env.meta[env.init_system].sys_data.coords.distance(env.meta[env.target_system].sys_data.coords);
|
|
let eff = 100. * opt_dist / total_dist;
|
|
println!("Total distance: {total_dist} ({eff:5.2}% efficiency)");
|
|
println!("{jcount} boosts used; {} jumps", stack.len());
|
|
|
|
|
|
} else {
|
|
println!("Unreachable after {} visits", env.visits);
|
|
std::process::exit(1);
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
}
|