Skip to content

Instantly share code, notes, and snippets.

@xybydy
Last active December 21, 2017 20:08
Show Gist options
  • Save xybydy/a8b4a1a6a7504b37c9e888d794580e0f to your computer and use it in GitHub Desktop.
Save xybydy/a8b4a1a6a7504b37c9e888d794580e0f to your computer and use it in GitHub Desktop.
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
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