Skip to content

Commit

Permalink
Merge pull request #23 from vcoppe/tsptw
Browse files Browse the repository at this point in the history
  • Loading branch information
xgillard committed Jun 9, 2023
2 parents 1edfa59 + 254ecb0 commit 8acd8f0
Show file tree
Hide file tree
Showing 475 changed files with 63,490 additions and 0 deletions.
57 changes: 57 additions & 0 deletions ddo/examples/tsptw/dominance.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright 2020 Xavier Gillard
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
// the Software, and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

use std::{sync::Arc, hash::Hash};

use ddo::Dominance;

use crate::state::TsptwState;

pub struct TsptwKey(Arc<TsptwState>);
impl Hash for TsptwKey {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
self.0.position.hash(state);
self.0.must_visit.hash(state);
}
}
impl PartialEq for TsptwKey {
fn eq(&self, other: &Self) -> bool {
self.0.position == other.0.position &&
self.0.must_visit == other.0.must_visit
}
}
impl Eq for TsptwKey {}

pub struct TsptwDominance;
impl Dominance for TsptwDominance {
type State = TsptwState;
type Key = TsptwKey;

fn get_key(&self, state: Arc<Self::State>) -> Option<Self::Key> {
Some(TsptwKey(state))
}

fn nb_dimensions(&self, _: &Self::State) -> usize {
1
}

fn get_coordinate(&self, state: &Self::State, _: usize) -> isize {
- (state.elapsed.latest() as isize)
}
}
52 changes: 52 additions & 0 deletions ddo/examples/tsptw/heuristics.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// Copyright 2020 Xavier Gillard
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
// the Software, and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

//! This module provides the implementation for some heuristics that may be used
//! to improve the behavior of the branch-and-bound-MDD solver for travelling
//! salesman problem with time windows.

use ddo::{StateRanking, WidthHeuristic, SubProblem};

use crate::state::TsptwState;

#[derive(Debug, Copy, Clone)]
pub struct TsptwRanking;

impl StateRanking for TsptwRanking {
type State = TsptwState;

fn compare(&self, sa: &Self::State, sb: &Self::State) -> std::cmp::Ordering {
sa.depth.cmp(&sb.depth)
}
}

pub struct TsptwWidth {
nb_vars: usize,
factor: usize,
}
impl TsptwWidth {
pub fn new(nb_vars: usize, factor: usize) -> TsptwWidth {
TsptwWidth { nb_vars, factor }
}
}
impl WidthHeuristic<TsptwState> for TsptwWidth {
fn max_width(&self, state: &SubProblem<TsptwState>) -> usize {
self.nb_vars * (state.depth as usize + 1) * self.factor

Check warning on line 50 in ddo/examples/tsptw/heuristics.rs

View workflow job for this annotation

GitHub Actions / clippy

casting to the same type is unnecessary (`usize` -> `usize`)

warning: casting to the same type is unnecessary (`usize` -> `usize`) --> ddo/examples/tsptw/heuristics.rs:50:25 | 50 | self.nb_vars * (state.depth as usize + 1) * self.factor | ^^^^^^^^^^^^^^^^^^^^ help: try: `state.depth` | = help: for further information visit https://rust-lang.github.io/rust-clippy/master/index.html#unnecessary_cast
}
}
109 changes: 109 additions & 0 deletions ddo/examples/tsptw/instance.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// Copyright 2020 Xavier Gillard
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
// the Software, and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

//! This module contains everything that is necessary to parse a TSP+TW instance
//! and turn it into a structs usable in Rust. Chances are high that this
//! module will be of little to no interest to you.

use std::{f32, fs::File, io::{BufRead, BufReader, Lines, Read}};

/// This structure, represents a timewindow. Basically it is nothing but a
/// closed time interval
#[derive(Debug, Copy, Clone)]
pub struct TimeWindow {
pub earliest: usize,
pub latest : usize
}
impl TimeWindow {
/// This is how you create a new time window
pub fn new(earliest: usize, latest: usize) -> Self {
Self { earliest, latest }
}
}

/// This structure represents the TSP with time window instane.
#[derive(Clone)]
pub struct TsptwInstance {
/// The number of nodes (including depot)
pub nb_nodes : u16,
/// This is the distance matrix between any two nodes
pub distances : Vec<Vec<usize>>,
/// This vector encodes the time windows to reach any vertex
pub timewindows: Vec<TimeWindow>
}

impl From<File> for TsptwInstance {
fn from(file: File) -> Self {
Self::from(BufReader::new(file))
}
}
impl <S: Read> From<BufReader<S>> for TsptwInstance {
fn from(buf: BufReader<S>) -> Self {
Self::from(buf.lines())
}
}
impl <B: BufRead> From<Lines<B>> for TsptwInstance {
fn from(lines: Lines<B>) -> Self {
let mut lc = 0;
let mut nb_nodes = 0_u16;
let mut distances = vec![];
let mut timewindows= vec![];

for line in lines {
let line = line.unwrap();
let line = line.trim();

// skip comment lines
if line.starts_with('#') || line.is_empty() {
continue;
}

// First line is the number of nodes
if lc == 0 {
nb_nodes = line.split_whitespace().next().unwrap().to_string().parse::<u16>().unwrap();
distances = vec![vec![0; nb_nodes as usize]; nb_nodes as usize];
}
// The next 'nb_nodes' lines represent the distances matrix
else if (1..=nb_nodes).contains(&lc) {
let i = (lc - 1) as usize;
for (j, distance) in line.split_whitespace().enumerate() {
let distance = distance.to_string().parse::<f32>().unwrap();
let distance = (distance * 10000.0) as usize;
distances[i][j] = distance;
}
}
// Finally, the last 'nb_nodes' lines impose the time windows constraints
else {
let mut tokens = line.split_whitespace();
let earliest = tokens.next().unwrap().to_string().parse::<f32>().unwrap();
let latest = tokens.next().unwrap().to_string().parse::<f32>().unwrap();

let earliest = (earliest * 10000.0) as usize;
let latest = (latest * 10000.0) as usize;

let timewind = TimeWindow::new(earliest, latest);
timewindows.push(timewind);
}

lc += 1;
}

TsptwInstance{nb_nodes, distances, timewindows}
}
}
146 changes: 146 additions & 0 deletions ddo/examples/tsptw/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
// Copyright 2020 Xavier Gillard
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of
// this software and associated documentation files (the "Software"), to deal in
// the Software without restriction, including without limitation the rights to
// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
// the Software, and to permit persons to whom the Software is furnished to do so,
// subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

//! This is the main entry point of the program. This is what gets compiled to
//! the tsptw binary.

use std::{fs::File, path::Path, time::{Duration, Instant}};

use clap::Parser;
use ddo::{Completion, TimeBudget, NoDupFringe, MaxUB, Solution, SimpleDominanceChecker, Problem, DefaultBarrierSolver, Solver};
use dominance::TsptwDominance;
use heuristics::{TsptwWidth, TsptwRanking};
use instance::TsptwInstance;
use model::Tsptw;
use relax::TsptwRelax;

mod instance;
mod state;
mod model;
mod relax;
mod heuristics;
mod dominance;

#[cfg(test)]
mod tests;

/// TSPTW is a solver based on branch-and-bound mdd which solves the travelling
/// salesman problem with time windows to optimality.
///
/// The implementation of tsptw is based on
/// 'ddo: a generic and efficient framework for MDD-based optimization' (IJCAI20)
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Args {
/// The path to the TSP+TW instance that needs to be solved.
instance: String,
/// The maximum width of an mdd layer. The value you provide to this
/// argument will serve as a multiplicator to the default. Hence,
/// providing an argument value `width == 5` for an instance having 20
/// "cities" to visit, means that the maximum layer width will be 100.
/// By default, the number of nodes equates to the number of unassigned
/// variables.
#[clap(short, long)]
width: Option<usize>,
/// How many threads do you want to use to solve the problem ?
#[clap(short, long)]
threads: Option<usize>,
/// How long do you want the solver to keep working on your problem ?
/// (in seconds)
#[clap(short, long)]
duration: Option<u64>,
}

fn main() {
let args = Args::parse();
let inst = TsptwInstance::from(File::open(&args.instance).unwrap());
let pb = Tsptw::new(inst);
let relax = TsptwRelax::new(&pb);
let width = TsptwWidth::new(pb.nb_variables(), args.width.unwrap_or(1));
let dominance = SimpleDominanceChecker::new(TsptwDominance);
let cutoff = TimeBudget::new(Duration::from_secs(args.duration.unwrap_or(u64::MAX)));
let mut fringe = NoDupFringe::new(MaxUB::new(&TsptwRanking));
let mut solver = DefaultBarrierSolver::custom(
&pb,
&relax,
&TsptwRanking,
&width,
&dominance,
&cutoff,
&mut fringe,
args.threads.unwrap_or(num_cpus::get())
);

let start = Instant::now();
let outcome = solver.maximize();
let finish = Instant::now();

let instance = instance_name(&args.instance);
let nb_vars = pb.nb_variables();
let lb = objective(solver.best_lower_bound());
let ub = objective(solver.best_upper_bound());
let solution = solver.best_solution();
let duration = finish - start;

print_solution(&instance, nb_vars, outcome, &lb, &ub, duration, solution);
}
fn print_solution(name: &str, n: usize, completion: Completion, lb: &str, ub: &str, duration: Duration, solution: Option<Solution>) {
println!("instance : {name}");
println!("status : {}", status(completion));
println!("lower bnd: {lb}");
println!("upper bnd: {ub}");
println!("duration : {}", duration.as_secs_f32());
println!("solution : {}", solution_to_string(n, solution));
}
fn instance_name<P: AsRef<Path>>(fname: P) -> String {
let name = fname.as_ref().file_name().unwrap().to_str().unwrap();
let bench= fname.as_ref().parent().unwrap().file_name().unwrap().to_str().unwrap();

format!("{}/{}", bench, name)
}
fn objective(x: isize) -> String {
match x {
isize::MIN => "+inf".to_string(),
isize::MAX => "-inf".to_string(),
_ => format!("{:.2}", -(x as f32 / 10_000.0_f32))
}
}
fn status(completion: Completion) -> &'static str {
if completion.is_exact {
"Proved"
} else {
"Timeout"
}
}
fn solution_to_string(nb_vars: usize, solution: Option<Solution>) -> String {
match solution {
None => "No feasible solution found".to_string(),
Some(s)=> {
let mut perm = vec![0; nb_vars];
for d in s.iter() {
perm[d.variable.id()] = d.value;
}
let mut txt = String::new();
for v in perm {
txt = format!("{} {}", txt, v);
}
txt
}
}
}
Loading

0 comments on commit 8acd8f0

Please sign in to comment.