Last active
December 21, 2017 20:08
-
-
Save xybydy/a8b4a1a6a7504b37c9e888d794580e0f to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Node_Name,X_Coordination,Y_Coordination,Quantity | |
A1,100,166,40 | |
A2,162,158,50 | |
A3,198,173,10 | |
A4,130,47,30 | |
Customer,121,114,90 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import pathlib | |
import argparse | |
from xlrd import open_workbook | |
from itertools import permutations # Permuation function | |
from math import sqrt # Square-root function | |
import matplotlib.pyplot as plt # Library to show the plot graphic | |
class Node: | |
""" | |
'Node' class represents all the entities (factories and customer) on the formulation. | |
Parameters and Attributes | |
_________________________ | |
name: string | |
The name of the 'Node'. | |
x: string or int | |
The x coordinates of the 'Node'. | |
y: string or int | |
The y coordinates of the 'Node'. | |
""" | |
def __init__(self, name, x, y): | |
""" | |
The '__init__' method initializes the class. | |
""" | |
self.x = int(x) # if the parameters is a string, automatically converts it to an integer | |
self.y = int(y) # if the parameters is a string, automatically converts it to an integer | |
self.name = name | |
def __repr(self): | |
""" | |
The '__repr__' method returns the name of the Node whenever Node is called. | |
""" | |
return self.name | |
def __add__(self, other): | |
""" | |
The '__add__' is a special method. It gets the euclidan length of two nodes wih '+' symbol. | |
Parameters | |
__________ | |
other: class Node | |
The other 'Node' to calculate the distance with. | |
""" | |
return sqrt((self.x - other.x) ** 2 + (self.y - other.y) ** 2) # euclidian formulation's implementation | |
def __sub__(self, other): | |
""" | |
The '__sub__' is a special method. It gets the the middle point of two nodes given with '-' symbol. | |
Parameters | |
__________ | |
other: class Node | |
The other 'Node' to calculate the middle point's coordinates with. | |
""" | |
return (self.x + other.x) / 2, (self.y + other.y) / 2 # middle point formulation's implementation | |
class Customer(Node): | |
""" | |
The subclass of 'Node' class which inherits all of 'Nodes's attributes. | |
Parameters and Attributes | |
_________________________ | |
demand: string or int | |
The demand of the customer. | |
""" | |
def __init__(self, name, x, y, demand): | |
""" | |
The '__init__' method also have attribute and parameter named 'demand' in addition to 'Node's. | |
""" | |
super(Customer, self).__init__(name, x, y) | |
self.demand = int(demand) # if the parameters is a string, automatically converts it to an integer | |
class Factory(Node): | |
""" | |
The subclass of 'Node' class which inherits all of 'Nodes's attributes. | |
Parameters and Attributes | |
_________________________ | |
storage: string or int | |
The storage of the factory/warehouse. | |
""" | |
def __init__(self, name, x, y, storage): | |
""" | |
The '__init__' method also have attribute and parameter named 'storage' in addition to 'Node's. | |
""" | |
super(Factory, self).__init__(name, x, y) | |
self.storage = int(storage) | |
class Rota: | |
""" | |
'Rota' class is the route combination of the warehouses which meets the demand of the customer given. | |
Parameters | |
__________ | |
args: list or tuple | |
the list of factories/warehouses of the combination of 'Factory' classes. | |
customer: Customer class | |
the customer who needs the stocks, must be 'Customer' class. | |
Attributes | |
____________ | |
factories: Factory class | |
the list of factories on the route | |
mesafe: int | |
the calculated length of the route | |
customer: Customer class | |
the final destination of the route, the customer. | |
Properties | |
__________ | |
node_num: int | |
Returns number of the factories are in the route. | |
""" | |
def __init__(self, *args, customer): | |
""" | |
The '__init__' method initializes the class with given parameters, | |
then calculates the length of the route from the factories to customer. | |
""" | |
self.factories = args | |
self.mesafe = 0 | |
self.customer = customer | |
self.__rota_mesafe() | |
def __repr__(self): | |
""" | |
Returns the route's human readable name. e.g. A2 - A4 - A3 - Customer: 355.70 | |
""" | |
return ' - '.join([factory.name for factory in self.factories]) + f' - {self.customer.name}: {self.mesafe:.2f}' | |
def __len__(self): | |
""" | |
Returns the length of the route. | |
""" | |
return self.mesafe | |
def __eq__(self, other): | |
""" | |
To make the route comparable with another routes. It compares it with the length of the route. | |
""" | |
return self.mesafe == other.mesafe | |
def __ne__(self, other): | |
""" | |
To implement total opposite behaviour of 'not equals (!=)' symbol. Unlike '=' symbol, | |
it checks if the nodes totally same, not just by the length of the routes. | |
""" | |
return not (self == other) | |
def __gt__(self, other): | |
""" | |
To implement 'greater than (>)' behaviour by it's length. | |
""" | |
return self.mesafe > other.mesafe | |
def __lt__(self, other): | |
""" | |
To implement 'less than (<)' behaviour by it's length. | |
""" | |
return self.mesafe < other.mesafe | |
def __ge__(self, other): | |
""" | |
To implement 'great or equals (>=)' behaviour by it's length. | |
""" | |
return self.mesafe >= other.mesafe | |
def __le__(self, other): | |
""" | |
To implement 'less or equals (<=)' behaviour by it's length. | |
""" | |
return self.mesafe <= other.mesafe | |
def __rota_mesafe(self): | |
""" | |
Class method which calculates the total length of the route and then assigns the result to self.mesafe attribute. | |
Basically the behaviour is, | |
The method gets in the loop from 1(remember that all indices starts with 0) to the length of the factory list on the route | |
By '+' symbol between the 'Nodes' gets the euclidian distance (as it's implemented under Node class by __add__ method) within the routes | |
and then it adds the result to 'mesafe' attribute. | |
After the loop within the factories, it calculates the distance with latest factory on the route with customer | |
and adds the result to 'mesafe' attribute. | |
""" | |
for index in range(1, len(self.factories)): | |
self.mesafe += self.factories[index - 1] + self.factories[index] | |
self.mesafe += self.customer + self.factories[-1] | |
@property | |
def node_num(self): | |
""" | |
Returns number of the factories are in the route. | |
""" | |
return len(self.factories) | |
class SCM: | |
""" | |
Main class of the application, which does all the magic. | |
Parameters | |
__________ | |
filename: string (default: 'data.txt') | |
Name of the source file. It must be in the same directory with the application. | |
Attributes | |
__________ | |
factories: list | |
The list of the all factories (class 'Factory') in the source file. | |
customer: | |
The customer, class 'Customer' | |
rotalar: list | |
The route list which covers the 'Customer's demand | |
girdi: list | |
Raw list all the elements in the source file | |
""" | |
def __init__(self, filename='data.txt'): | |
self.factories = list() | |
self.customer = None | |
self.rotalar = list() | |
self.filename = filename | |
if pathlib.Path(self.filename).suffix[1:] in ('xlsx', 'xls'): | |
self.read_excel() | |
elif pathlib.Path(self.filename).suffix[1:] == 'txt': | |
self.read_txt() | |
else: | |
print('Unsupported filetype :/') | |
raise SystemExit | |
with open(self.filename) as f: # we open the file on this line | |
""" | |
Explanation of self.girdi: | |
Reads and splits each line on the file except the first line which has the headers, then | |
splits the each line by commas and builds a matrix of nodes. | |
Sample output is below, | |
Each line has Node_name, x_coord, y_coord, amount. | |
[['A1', '100', '166', '40'], | |
['A2', '162', '158', '50'], | |
['A3', '198', '173', '10'], | |
['A4', '130', '47', '30'], | |
['Customer', '121', '114', '90']] | |
""" | |
for i in self.girdi[:-1]: | |
""" | |
Gets in the loop of girdi matrix except the last row, which is always the customer. | |
Turns every raw line to Factory class instance and appends the instances to 'factories' attribute. | |
""" | |
self.factories.append(Factory(*i)) | |
self.customer = Customer(*self.girdi[-1]) # Makes the last row the Customer. | |
self.qty_bul() # calls qty_bul method | |
self.report() | |
def read_excel(self): | |
excel_file = open_workbook(self.filename, encoding_override='utf-8') # opens the excel file | |
sheet = excel_file.sheet_by_index(0) # grabs the first sheet in the file | |
self.girdi = [[sheet.cell(x, y).value for y in range(sheet.ncols)] for x in range(1, sheet.nrows)] | |
def read_txt(self, delimeter=','): | |
with open(self.filename) as f: | |
self.girdi = [i.split(delimeter) for i in f.read().split('\n')][1:] | |
def qty_bul(self): | |
""" | |
Tries every permutation of the factories and compares the storages with customer demand. | |
Appends the matched routes to 'rotalar' attribute as 'Rota' instances. | |
""" | |
for x in range(1, len( | |
self.factories)): # Since we have to start with permutations of the factories, we start our loop from 1 instead of 0. | |
for y in permutations(self.factories, x): | |
of = 0 | |
for v in y: | |
of += v.storage # sums every factory's storage on the route | |
if of == self.customer.demand: # checks if the route's storage meets customer demand | |
self.rotalar.append( | |
Rota(*y, customer=self.customer)) # appends the matched 'Rota' instance to 'rotalar' instance. | |
@property | |
def en_kisa(self): | |
""" | |
Return the shortest route. | |
""" | |
if len(self.rotalar) > 1: | |
return min(self.rotalar) | |
def draw_graph(self): | |
""" | |
This function builds the plot graphic and show the shortest route on the plot. | |
""" | |
x = [factory.x for factory in self.factories] # makes a list of each x coordinates of the factories | |
y = [factory.y for factory in self.factories] # makes a list of each x coordinates of the factories | |
plt.plot(x, y, "bo") # build the blue factory points. | |
plt.plot(self.customer.x, self.customer.y, "ro") # build the red customer point. | |
for factory in self.factories: # gets in a loop within the factories | |
plt.annotate(' {}: ({},{})'.format(factory.name, factory.x, factory.y), | |
xy=(factory.x, factory.y)) # make annotations of the factories in 'NodeName: (x,y)' format. | |
plt.annotate(' {}: ({},{})'.format(self.customer.name, self.customer.x, self.customer.y), xy=( | |
self.customer.x, self.customer.y)) # make annotation of the customer in 'NodeName: (x,y)' format. | |
for index in range(1, | |
self.en_kisa.node_num): # gets in the loop from 1 to number of the nodes of the shortest path | |
plt.plot((self.en_kisa.factories[index - 1].x, self.en_kisa.factories[index].x), ( | |
self.en_kisa.factories[index - 1].y, | |
self.en_kisa.factories[index].y),'b') # makes the line between the nodes | |
plt.annotate(' {:.2f} br'.format(self.en_kisa.factories[index - 1] + self.en_kisa.factories[index]), | |
xy=self.en_kisa.factories[index - 1] - self.en_kisa.factories[ | |
index]) # makes the annotation of the line in the middle of the line | |
plt.plot((self.en_kisa.factories[-1].x, self.customer.x), ( | |
self.en_kisa.factories[-1].y, self.customer.y),'r') # makes the line between latest factory and the customer | |
plt.annotate(' {:.2f} br'.format(self.en_kisa.factories[-1] + self.customer), xy=self.en_kisa.factories[ | |
-1] - self.customer) # annotation of the line between the latest factory and the customer | |
plt.show() # shows the plot | |
def report(self): | |
""" | |
Makes the report. | |
""" | |
print(f'Customer demand: {self.customer.demand}\n') | |
print('Pallets available on D locations') | |
for factory in self.factories: | |
print(factory.name, ":", factory.storage) | |
print() | |
if len(self.rotalar) == 0: | |
print("Couldn't find any route") | |
raise SystemExit | |
else: | |
print("Possible routes that meet customer demand: ") | |
for route in self.rotalar: | |
print(route) | |
print("\nOptimum Route:", self.en_kisa, "with minimum distance") | |
self.draw_graph() | |
def parse(): | |
parser = argparse.ArgumentParser(description='Supply Chain Management Project', epilog='Fatih Karsli 2017') | |
parser.add_argument('file', default='data.txt', help='Filename', nargs='?') | |
args = parser.parse_args() | |
return args.file | |
if __name__ == "__main__": | |
file = parse() | |
s = SCM(filename=file) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment