从本章开始进入规范性分析的核心问题:Flow Shop Scheduling,可以理解为流水车间问题,即怎样合理安排生产顺序以实现利润最大化和成本最小化。
本章的案例是一家生产牙膏的企业,由于产品种类的增加,以前的规划方法逐渐到达了极限。现在我们需要利用python为这家牙膏厂开发合适的规划方法。
该公司的生产概况如下:
一个集合N = {1, 2, . ... , n}的n个订单要在m不同的工位上依次处理,每个工位只有一台机器可用。所有订单的操作顺序都是相同的。所有机器都是连续可用的,一次可以处理一个订单。机器之间的缓冲可能性被认为是无限的。然而,在机器上处理订单之前,必须对机器进行整备或设置。
生产规划的一个重要原则就是以客户为导向,我们总是希望尽可能准时完成订单的生产和交付。然而,一些客户比其他客户更重要,这种重要性体现在,这些客户的订单延期会造成高额的罚款。
我们现在的工作就是找到一个合适的启发式算法,怎么找?一方面我们需要知道订单数量n和机器数量m,另一方面需要每台机器加工每个订单的加工时间和整备时间。
首先我们从导入数据开始,所有需要的信息都存储在一个json文件中。json文件可以类比python中的字典,实际上是一个“字典的字典”,
1 读入json数据
文件名: "InputFlowshopSIST.json"
请找出哪个模块可以用来读入json文件。你怎样才能读入这个json文件,以便进一步使用它?
答案是json模块(也叫库),安装完之后导入即可:
import json
with open('InputFlowshopSIST.json') as json_file:
data = json.load(json_file) # As a dictionary
print(data)
json库读取数据时,首先要用with open('路径') as fileName:
来将json文件在python中打开,fileName
就相当于这个json文件在编译器里的名字,然后调用json.load(fileName)
将json文件导入并赋值给另一个变量data
,这个变量的类型是字典。
上图中一共有4个键值对,四个键分别是:文件名Name,机器数量nMachines=5,订单数量nJobs=11,和订单Jobs。注意,键Jobs对应的值是一个列表,列表的元素是字典,每个字典容纳了每个订单的必要信息,例如-
- SetupTimes表示这个订单在所有5台机器上相应的整备时间(整备时间以列表形式给出,列表中的元素是有顺序的),
- ProcessingTimes表示这个订单在每台机器上的加工时间(加工时间以列表形式给出,列表中的元素是有顺序的
- DueDate表示这个订单的交货期限
- TardCosts表示延期交货的罚款数额
2 @property修饰器的用法简介
@property
是一种修饰器,用于修饰方法或函数,我们可以用@property
来创建只读属性,在函数定义的上一行加入这个装饰器,就会把这个函数转换成相同名称的只读属性。这样可以防止原本定义的属性被修改。
被装饰的函数在调用时,不能用普通函数的调用方式来调用,而是要用属性的调用方式,也就是说调用时不能加括号
下面展开来说说:
# 定义学生类
class Student:
def __init__(self, name, score):
self.Name = name
self.__Score = score
# 修改学生的分数属性:
s = Student('Bob', 59)
print(s)
s.Score = 60
print(s.Score)
但是这样修改有隐患,我们可以给学生的分数属性赋任何值。为了消除隐患我们通常会用get和set方法,即在类的定义中增加setScore和getScore两个方法。这里的get方法仅仅读取类的属性。代码如下:
class Student:
def __init__(self, name, score):
self.Name = name
self.__Score = score
def getScore(self):
return self.__Score
def setScore(self, newScore):
# 在set方法中,如果newScore不满足条件,则会报错
if newScore < 0 or newScore > 100:
raise ValueError('Invalid score')
self.__Score = newScore
s2 = Student('Alice', 99)
print(s2.getScore())
s2.setScore(100)
print(s2.getScore())
s2.setScore(120)
*注意:通常情况下这样做是可行的,而且更容易理解。无论进行读还是写操作,都可以通过相应的函数调用来实现
为了图省事,我们会引入修饰器,这样我们在修改属性信息的时候就不用通过函数来操作了。原理是我们把set和get函数装饰成属性来调用。试想一下,借助装饰器,我们实际上结合了前面两种方式的优点,既能享受给属性赋值的方便又能保证分数不会超出范围。两全其美!
class Student:
def __init__(self, name, score):
self.Name = name
self.__Score = score
@property
def score(self):
# 这个score函数相当于get方法,没有参数
return self.__Score
@score.setter
def score(self, newScore):
# 这个score是set方法,用@score.setter来修饰,@score.setter是前面@property修饰后的副产品
if newScore < 0 or newScore > 100:
raise ValueError('Invalid score')
self.__Score = newScore
s3 = Student('Cindy', 88)
print(s3.score)
s3.score = 90
print(s3.score)
3 用代码描述真实世界
第一节只是基础,我们掌握了如何用json包读入数据。在实际操作中我们很少会直接这样写,更普遍的用法是将第一节的逻辑封装在类(或者函数,类本质上是特殊的函数)中,后期直接通过接口调用这个类。这样可以避免代码重复,比如说,在这个json文件中有11个订单,可以想象成今年的第一季度工厂收到了11个订单,那么第二季度的生产中,工厂会收到其他的订单。如果现在我们将代码封装在类中,第二季度的订单到来的时候直接调用类即可。
思考一下,我们应该定义哪几个类来描述json文件中的各种信息?注意一定要全面!
通过观察不难发现,我们首先需要一个“机器”类class DataMachine
,我们一共有5台机器,每台都有一个唯一的编号;另一方面,我们还需要创建一个“订单”类class DataJob
,用于描述订单的信息,这个类可能会稍微复杂些。
记住要定义__init__()
和__str__()
函数。此外,来自json文件的输入数据应该反映在类中。
class DataMachine:
def __init__(self, machineId):
self.MachineId = machineId
# 返回机器的序号
def __str__(self):
return f'Machine {self.MachineId}'
class DataJob:
def __init__(self, idJob, setupTimes, processingTimes, dueDate, tardinessCost):
self.__JobId = idJob
self.__SetupTimes = setupTimes
self.__ProcessingTimes = processingTimes
self.__DueDate = dueDate
self.__TardCost = tardinessCost
# 返回订单job的序号和这个订单需要经过几步操作
def __str__(self):
return f'Job {self.__JobId} with {len(self.__ProcessingTimes)} operations.\n'
# __str__函数只是返回一个泛泛的信息,如:订单1有5个操作
# 现在写几个函数,输出Job对象的特定信息,例如id, 某台机器的整备时间和处理时间
@property
def JobId(self):
# 这个函数的目的是可以用job对象.JobId()来直接获取订单号。因为类中定义的订单号是私有的,在别的类中没有访问权限
return self.__JobId
@JobId.setter
def JobId(self, newId):
self.__JobId = newId # 通过这个函数我们可以修改订单的编号。跟上一个函数同名,但是参数表不同,编译器会根据参数表来区分同名函数
@property
def TardCost(self):
return self.__TardCost
@property
def DueDate(self):
return self.__DueDate
@property
def Operations(self):
# 这个函数会输出一个元组的列表,元组第一个元素是加工订单的机器的编号,第二个元素是加工时长
# 注意这里的Operations和str函数里的不同,str中仅仅输出订单被处理几次,这里输出每次加工用到机器编号和时长
return [(optId, processingTime) for optId, processingTime in enumerate(self.__ProcessingTimes)]
# 下面这两个函数分别输出某个订单对象在某台机器上的整备时间和加工时间。注意看json截图,这两个时间保存在列表中
@property
def SetupTimes(self, position):
return self.__SetupTimes[position]
@property
def ProcessdingTimes(self, position):
return self.__ProcessingTimes[position]
# 构造一个DataJob对象
newJob = DataJob(1, [22,33,44,55,66], [1,2,3,4,5], 290, 50)
print(newJob)
newJob.JobId = 3
print(newJob.JobId)
# 用调用属性的方式调用函数
print(newJob.DueDate)
print(newJob.Operations)
4 定义一个类,用于导入数据
创建一个InputData
类,该类处理.json文件并创建前一节中的相应类对象。
创建这个类的时候需要考虑两件事:
- 这个类的对象如何构造?
导入数据通常需要一个路径,也就是文件存储的位置,每次只需要粘贴路径就能导入相应的文件 - 这个类要实现哪些具体的操作?
由于我们处理的都是json文件,所以需要用到json.load()
。接下来就是在InputData类中构造两个类的对象,我选择的方案是分别在两个空列表中,借助for循环,每次循环都要执行一次构造函数,然后把构造出来的实例放到列表中。注意这个函数是不需要返回值的。
import json
class InputData:
def __init__(self, path):
self.__path = path
# 构造函数中调用这个类的成员函数,即:实例化的同时执行成员函数!!
self.DataLoad()
def DataLoad(self):
with open(self.__path, 'r') as inputFile:
inputData = json.load(inputFile)
self.m = inputData['nMachines']
self.n = inputData['nJobs']
self.InputJobs = []
for job in inputData['Jobs']:
# 这一步并不难,需要熟悉json文件的嵌套
self.InputJobs.append(DataJob(job['Id'], job['SetupTimes'],
job['ProcessingTimes'], job['DueDate'], job['TardCosts']))
# 注意:InputData的对象的属性是个列表,调用的时候需要写索引
self.InputMachines = []
for k in range(self.m):
self.InputMachines.append(DataMachine(k))
# 构造一个InputData类的对象
data = InputData("InputFlowshopSIST.json")
# 调用data的InputJobs属性,这个属性是个列表,列表元素是对象。
# 由对象组成 的列表不能直接输出,而是要彻底实例化,即说清楚具体的那个元素
print(data.InputJobs[1])
print(data.InputJobs)
print(data.InputMachines[1])
*注意:在我们的InputData类中,囊括了json文件中的全部数据,既有订单job的信息,又有机器machine的信息。这显然不符合我们的要求,因为在做生产排期时,我们需要把输入信息处理一下,只关心订单在每台机器上的整备和加工时间以及期限和罚款。因此我们需要一个输出数据的类,叫做
OutputJob
,下节会讲!
5 把订单信息job筛选出来
创建一个OutputJob类,用于描述预定的订单信息。
首先来明确继承关系,OutputJob应该是DataJob的子类;然后需要明确,OutputJob的数据来源是InputData,所以我们在定义OutputJob类时需要导入InputData。
# from InputData import *
class OutputJob(DataJob):
def __init__(self, dataJob):
super().__init__(dataJob.JobId, [dataJob.SetupTimes(i) for i in range(len(dataJob.Operations))],
[dataJob.ProcessingTimes(i) for i in range(len(dataJob.Operations))], dataJob.DueDate, dataJob.TardCost)
self.StartSetups = [0]*len(self.Operations)
self.EndSetups = [0]*len(self.Operations)
self.StartTimes = [0]*len(self.Operations)
self.EndTimes = [0]*len(self.Operations)
self.Tardiness = 0