Skip to content
This repository has been archived by the owner on May 19, 2023. It is now read-only.

Commit

Permalink
Merge release 1.0.0
Browse files Browse the repository at this point in the history
  • Loading branch information
netotz committed May 19, 2020
1 parent 82a027f commit 97c3085
Show file tree
Hide file tree
Showing 90 changed files with 33,327 additions and 17 deletions.
7 changes: 5 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Generated instance
*.dat
# Experimental results
*.csv

# PyLint file
.pylintrc

# Byte-compiled / optimized / DLL files
__pycache__/
Expand Down
6 changes: 6 additions & 0 deletions file_handling/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
'''
Package for handling files and directories (folders).
'''

from .file_io import write_instance, read_instance, write_results
from .path import list_files
76 changes: 76 additions & 0 deletions file_handling/file_io.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
'''
Module for writing and reading instances to and from files.
'''

import os
import csv
from typing import List

from models import PDPInstance, Point
from .path import generate_filename, get_filepath

def write_instance(instance: PDPInstance):
'''
Receives a PDP Instance and writes it to a file.
'''
index = 0
while True:
filename = generate_filename(instance.n, instance.p, index)
filepath = get_filepath(filename)
if os.path.exists(filepath):
index += 1
else:
break

folder = get_filepath('')
if not os.path.exists(folder):
os.makedirs(folder)

try:
with open(filepath, 'w') as file:
file.write(str(instance))
except (IOError, OSError) as error:
print('The instance could not be written:\n', error)

def read_instance(filename: str) -> PDPInstance:
'''
Reads a file that contains a PDP instance and returns its object.
'''
filepath = get_filepath(filename)
try:
# if file is empty
if os.stat(filepath).st_size == 0:
print(' File %s is empty.' % filename)
return None

with open(filepath, 'r') as file:
# read every line of the file and parse it to constructor's arguments of Point class
points = [Point(*map(int, line.split())) for line in file]

# get p from filename
p = int(filename.split('_')[1])
# return an object of PDPInstance
return PDPInstance(p, points)
except FileNotFoundError as error:
print(' ', error)
return None
except ValueError:
print(' File %s has invalid format.' % filename)
return None

def write_results(filename: str, data: List[List[str]]):
'''
Writes a CSV file containing the results of an experiment.
'''
folder = get_filepath('', 'results')
if not os.path.exists(folder):
os.makedirs(folder)

filepath = get_filepath(filename, 'results')
try:
with open(filepath, 'w', newline='') as file:
writer = csv.writer(file)
for row in data:
writer.writerow(row)
except (IOError, OSError) as error:
print('The results could not be written:\n', error)
54 changes: 54 additions & 0 deletions file_handling/path.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
'''
Module for handling directories and getting paths.
'''

import os
import random
from typing import List

def generate_filename(n: int, p: int, index: int = 0) -> str:
'''
Generates a name for an instance's file:
<n>_<p>_<index>.dat
'''
return str(n) + '_' + str(p) + '_' + f'{index:02d}' + '.dat'

def get_filepath(filename: str, folder: str = 'instances') -> str:
'''
Returns the path of a file based on it's name.
'''
current_dir = os.path.dirname(__file__)
filepath = os.path.join(current_dir, '..', folder, filename)
return os.path.abspath(filepath)

def list_files(size: int, number: int) -> List[str]:
'''
Returns a list of number .dat files in the instances/ subdirectory
according to the specified size.
'''
current_dir = os.path.dirname(__file__)
subdirectory = os.path.abspath(os.path.join(current_dir, '..', 'instances'))
try:
files = os.listdir(subdirectory)
except FileNotFoundError:
os.makedirs(subdirectory)
return list_files(size, number)
else:
prefix = str(size) + '_'
suffix = '.dat'

filtered_files = [
file for file in files
if file.startswith(prefix) and file.endswith(suffix)
]

if not filtered_files:
print(f' error: there are no instances of size {size}')
return []
elif len(filtered_files) == number:
return filtered_files
elif len(filtered_files) > number:
return random.sample(filtered_files, number)
else:
print(f' error: there are only {len(filtered_files)} files, not {number}')
8 changes: 8 additions & 0 deletions generate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
'''
Generate an instance from the command line and save it to a file.
'''

from generator import generate_instance, parse_arguments

args = parse_arguments()
generate_instance(*args)
3 changes: 0 additions & 3 deletions generator/README.md

This file was deleted.

6 changes: 6 additions & 0 deletions generator/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
'''
Package that contains necessary functions to generate an instance.
'''

from .generate_instance import generate_instance
from .cl_argsparse import parse_arguments
113 changes: 113 additions & 0 deletions generator/cl_argsparse.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
'''
Command line arguments parser.
Module to parse arguments from the command line.
The parsed arguments are then used in other module to generate an instance.
'''

import sys
import argparse
from typing import Tuple

from validations import is_valid_n, is_percentage, is_positive_int, are_valid_dimensions, is_valid_p

def parse_arguments() -> Tuple[int, int, Tuple[int, int], int, int]:
'''
An ArgumentParser object receives arguments from the command line
and returns them in a tuple of 4 elements.
(n: int, p: int, dimensions: Tuple[int, int], instances: int)
If no arguments are given the program will end.
'''
# parser's description display when --help is used
description = 'Generates an random instance with the following arguments:'
# instantiate argument parser
parser = argparse.ArgumentParser(description=description)

# remove optional arguments from parser but store it to append it at the end
#* this 'hack' is used because otherwise the
#* required exclusive arguments are displayed as optional when they aren't
optional = parser._action_groups.pop()

# required arguments
# both 'n' and 'p' must be specified
required = parser.add_argument_group('requiered arguments')
required.add_argument(
'n',
type=is_valid_n,
help='total number of candidate points'
)
required.add_argument(
'p',
type=is_percentage,
help='percentage of points to select'
)

# required exclusive arguments
# only one argument needs to be specified
shape = required.add_mutually_exclusive_group(required=True)
shape.add_argument(
'-s', '--square',
metavar='length',
type=is_positive_int,
help='locate the n points in a squared plane of dimensions length * length at most'
)
shape.add_argument(
'-r', '--rectangle',
metavar=('length', 'width'),
nargs=2,
type=is_positive_int,
help='locate the n points in a rectangular plane of dimensions length * width at most'
)

# optional arguments
optional.add_argument(
'-n', '--number',
type=is_positive_int,
default=1,
help='number of instances to generate, default to 1'
)
optional.add_argument(
'-v', '--verbose',
type=int,
default=0,
choices=(0, 1, 2),
help='''increase output verbosity:
0 = no output (default).
1 = outputs instance generation and writing.
2 = same as 1 and shows a plot.'''
)
# append optional arguments to parser to display at the end
parser._action_groups.append(optional)

# if no arguments are given, display help as if -h was used
if len(sys.argv) == 1:
parser.print_help()
sys.exit(1)

# parse arguments from command line
arguments = parser.parse_args()

# if chosen shape is squared
if arguments.square:
dimensions = (arguments.square, arguments.square)
# if chosen shape is rectangular
else:
dimensions = tuple(arguments.rectangle)
n = arguments.n
percentage = arguments.p
p = int(percentage * n)

# validate dimensions and p
msg = ' error: invalid'
if not are_valid_dimensions(n, dimensions):
print(f'{msg} dimensions: x*y must be greater or equal to n')
sys.exit(1)
elif not is_valid_p(p):
print(f'{msg} p value: must be 2 or greater')
sys.exit(1)
else:
# return parsed arguments gathered in a tuple
return (arguments.n, p, dimensions, arguments.number, arguments.verbose)
48 changes: 48 additions & 0 deletions generator/generate_instance.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
'''
Module to generate an instance for the PDP.
'''

from typing import Tuple

from models import PDPInstance
from models.plotter import plot_instance
from file_handling import write_instance

def generate_instance(n: int, p: int, dimensions: Tuple[int, int], number: int, verbose: int):
'''
Generates an random instance based on the arguments parsed:
n: total number of candidate points.
p: points to choose from n.
dimensions: maximum values for coordinates (x, y).
number: instances to generate.
verbose: output verbosity.
The generated instance is saved to a .dat file.
'''
x_max, y_max = dimensions

if not verbose:
str_gen = ''
str_wr = ''
str_done = ''
else:
str_gen = 'Generating instance... '
str_wr = 'Writing instance to file... '
str_done = 'done.'

for _ in range(number):
print(str_gen, end='', flush=True)
instance = PDPInstance.random(n, p, x_max, y_max)
print(str_done)

print(str_wr, end='', flush=True)
write_instance(instance)
print(str_done)

if verbose == 2:
plot_instance(instance.points)
3 changes: 3 additions & 0 deletions heuristic/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
'''
Package of the implemented algorithms of heuristics for the PDP.
'''
53 changes: 53 additions & 0 deletions heuristic/constructive.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
'''
Module of the implementations of constructive heuristics for the PDP.
'''

from models import PDPInstance, Solution
from models.plotter import plot_instance_solution

def greedy_construction(instance: PDPInstance, verbose: bool = False) -> Solution:
'''
Starting by choosing the 2 farthest points,
the algorithm adds the farthest point to the current solution until p is reached.
Returns a list of the p chosen points.
'''
# copy all the instance's points
candidates = list(instance.points)
# initialize the solution with the 2 farthest points
solution = list(instance.get_farthest_points())
if verbose:
print('Greedy Construction (GC)')
print(' Solution starts with the 2 farthest points:')
print(f' S = {solution}')
plot_instance_solution(instance.points, solution)

# remove them from the candidates
candidates.remove(solution[0])
candidates.remove(solution[1])

# until solution has p points
while len(solution) < instance.p:
len_solution = len(solution)
# add the point farthest to the current solution
maximum = max(
# the distance between a point and a set is
# the smallest of the distances between the point and the members of the set
((cp, min(instance.distances[cp.index][sp.index] for sp in solution))
for cp in candidates),
key=lambda x: x[1]
)
chosen_point = maximum[0]
# add the new point
solution.append(chosen_point)
# remove it from candidates
candidates.remove(chosen_point)

if verbose:
print(f' p = {len_solution}\n')
print(' Add farthest point to current solution:')
print(f' x = {repr(chosen_point)}')
print(f' S = {solution}')
plot_instance_solution(instance.points, solution)

return solution
Loading

0 comments on commit 97c3085

Please sign in to comment.