之前在 Pyomo介绍-CSDN博客 中以饮食为例介绍过Pyomo的使用,执行以下命令:
pyomo solve --solver=glpk test_pyomo_linear_programming.py ../test_data/diet.dat
直接执行以上命令,不便之处有以下几点:
(1).不能直接解析python文件,不便于后期与FastAPI集成;
(2).不支持传入额外参数;
(3).不能直接获取需要的结果,需要额外解析results.yml
针对以上问题,对原始代码进行了重新实现,包括:支持解析dat文件和json文件、支持输入参数、结果可直接通过print打印输出:
1.解析dat文件的实现如下:
def parse_dat(file):# creating a model objectmodel = ConcreteModel()# load data filedata = DataPortal()data.load(filename=file)# define sets and parametersmodel.F = Set(initialize=data['F'])model.N = Set(initialize=data['N'])model.c = Param(model.F, initialize=data['c'], within=PositiveReals)model.a = Param(model.F, model.N, initialize=data['a'], within=NonNegativeReals)model.V = Param(model.F, initialize=data['V'], within=PositiveReals)model.Nmin = Param(model.N, initialize=data['Nmin'], within=NonNegativeReals, default=0.0)model.Nmax = Param(model.N, initialize=data['Nmax'], within=NonNegativeReals, default=float('inf'))model.Vmax = Param(initialize=data['Vmax'], within=PositiveReals)return model
diet.dat文件内容如下:
param: F: c V :="Cheeseburger" 1.84 4.0 "Ham Sandwich" 2.19 7.5 "Hamburger" 1.84 3.5 "Fish Sandwich" 1.44 5.0 "Chicken Sandwich" 2.29 7.3 "Fries" .77 2.6 "Sausage Biscuit" 1.29 4.1 "Lowfat Milk" .60 8.0 "Orange Juice" .72 12.0 ;param Vmax := 75.0;param: N: Nmin Nmax :=Cal 2000 .Carbo 350 375Protein 55 .VitA 100 .VitC 100 .Calc 100 .Iron 100 . ;param a:Cal Carbo Protein VitA VitC Calc Iron :="Cheeseburger" 510 34 28 15 6 30 20"Ham Sandwich" 370 35 24 15 10 20 20"Hamburger" 500 42 25 6 2 25 20"Fish Sandwich" 370 38 14 2 0 15 10"Chicken Sandwich" 400 42 31 8 15 15 8"Fries" 220 26 3 0 15 0 2"Sausage Biscuit" 345 27 15 4 0 20 15"Lowfat Milk" 110 12 9 10 4 30 0"Orange Juice" 80 20 1 2 120 2 2 ;
2.解析json文件的实现如下:
def parse_json(file):model = ConcreteModel()data = DataPortal()data.load(filename=file)model.F = Set(initialize=data['sets']['F'])model.N = Set(initialize=data['sets']['N'])model.c = Param(model.F, initialize=data['params']['c'], within=PositiveReals)def parse_a(model, food, nutr):return data['params']['a'][food][nutr]model.a = Param(model.F, model.N, initialize=parse_a, within=NonNegativeReals)model.V = Param(model.F, initialize=data['params']['V'], within=PositiveReals)model.Nmin = Param(model.N, initialize=data['params']['Nmin'], within=NonNegativeReals, default=0.0)def parse_Nmax(model, nutr):val = data['params']['Nmax'][nutr]return val if val != "inf" else math.inf model.Nmax = Param(model.N, initialize=parse_Nmax, within=NonNegativeReals)model.Vmax = Param(initialize=data['params']['Vmax'], within=PositiveReals)return model
diet.json文件内容如下:
{"sets": {"F": ["Cheeseburger","Ham Sandwich","Hamburger","Fish Sandwich","Chicken Sandwich","Fries","Sausage Biscuit","Lowfat Milk","Orange Juice"],"N": ["Cal","Carbo","Protein","VitA","VitC","Calc","Iron"]},"params": {"c": {"Cheeseburger": 1.84,"Ham Sandwich": 2.19,"Hamburger": 1.84,"Fish Sandwich": 1.44,"Chicken Sandwich": 2.29,"Fries": 0.77,"Sausage Biscuit": 1.29,"Lowfat Milk": 0.6,"Orange Juice": 0.72},"V": {"Cheeseburger": 4.0,"Ham Sandwich": 7.5,"Hamburger": 3.5,"Fish Sandwich": 5.0,"Chicken Sandwich": 7.3,"Fries": 2.6,"Sausage Biscuit": 4.1,"Lowfat Milk": 8.0,"Orange Juice": 12.0},"Vmax": 75.0,"Nmin": {"Cal": 2000.0,"Carbo": 350.0,"Protein": 55.0,"VitA": 100.0,"VitC": 100.0,"Calc": 100.0,"Iron": 100.0},"Nmax": {"Cal": "inf","Carbo": 375.0,"Protein": "inf","VitA": "inf","VitC": "inf","Calc": "inf","Iron": "inf"},"a": {"Cheeseburger": {"Cal": 510.0,"Carbo": 34.0,"Protein": 28.0,"VitA": 15.0,"VitC": 6.0,"Calc": 30.0,"Iron": 20.0},"Ham Sandwich": {"Cal": 370.0,"Carbo": 35.0,"Protein": 24.0,"VitA": 15.0,"VitC": 10.0,"Calc": 20.0,"Iron": 20.0},"Hamburger": {"Cal": 500.0,"Carbo": 42.0,"Protein": 25.0,"VitA": 6.0,"VitC": 2.0,"Calc": 25.0,"Iron": 20.0},"Fish Sandwich": {"Cal": 370.0,"Carbo": 38.0,"Protein": 14.0,"VitA": 2.0,"VitC": 0.0,"Calc": 15.0,"Iron": 10.0},"Chicken Sandwich": {"Cal": 400.0,"Carbo": 42.0,"Protein": 31.0,"VitA": 8.0,"VitC": 15.0,"Calc": 15.0,"Iron": 8.0},"Fries": {"Cal": 220.0,"Carbo": 26.0,"Protein": 3.0,"VitA": 0.0,"VitC": 15.0,"Calc": 0.0,"Iron": 2.0},"Sausage Biscuit": {"Cal": 345.0,"Carbo": 27.0,"Protein": 15.0,"VitA": 4.0,"VitC": 0.0,"Calc": 20.0,"Iron": 15.0},"Lowfat Milk": {"Cal": 110.0,"Carbo": 12.0,"Protein": 9.0,"VitA": 10.0,"VitC": 4.0,"Calc": 30.0,"Iron": 0.0},"Orange Juice": {"Cal": 80.0,"Carbo": 20.0,"Protein": 1.0,"VitA": 2.0,"VitC": 120.0,"Calc": 2.0,"Iron": 2.0}}}
}
注:pyomo对json二维参数model.a需特殊处理
3.支持的输入参数如下:
def parse_args():parser = argparse.ArgumentParser(description="pyomo glpk diet")parser.add_argument("--file", required=True, type=str, help="*.dat file or *.json file")parser.add_argument("--number", type=int, default=5, help="must select number of food types from all food types")args = parser.parse_args()return args
4.主体实现如下:
def main(file, number):if Path(file).suffix.lower() == ".dat":model = parse_dat(file)elif Path(file).suffix.lower() == ".json":model = parse_json(file)else:raise ValueError(colorama.Fore.RED + f"unsupported file format: {file}")# define variablesmodel.x = Var(model.F, within=NonNegativeIntegers)model.y = Var(model.F, within=Binary)# define the cost objectivemodel.cost = Objective(expr=sum(model.c[i]*model.x[i] for i in model.F), sense=minimize)# define constraintdef nutrient_rule(model, j):value = sum(model.a[i,j]*model.x[i] for i in model.F)return inequality(model.Nmin[j], value, model.Nmax[j])model.nutrient_limit = Constraint(model.N, rule=nutrient_rule)def volume_rule(model):return sum(model.V[i]*model.x[i] for i in model.F) <= model.Vmaxmodel.volume = Constraint(rule=volume_rule)def select_rule(model):return sum(model.y[i] for i in model.F) == numbermodel.select = Constraint(rule=select_rule)def linking_upper_rule(model, f):return model.x[f] <= model.y[f] * 1e6model.linking_upper = Constraint(model.F, rule=linking_upper_rule)def linking_lower_rule(model, f):return model.x[f] >= model.y[f]model.linking_lower = Constraint(model.F, rule=linking_lower_rule)# model.pprint() # print model structure# solve the modelsolver = SolverFactory('glpk')results = solver.solve(model)# print(f"results: {results}")if results.solver.termination_condition != TerminationCondition.optimal:raise ValueError(colorama.Fore.RED + f"no optimal solution was found")# print resultprint(f"total cost: {value(model.cost):.2f}")count = 0print("selected food:")for f in model.F:v = int(value(model.x[f]))if v != 0:print(f" {f}: {v}")count += 1if count != number:raise ValueError(colorama.Fore.RED + f"solution result is wrong, number of food types does not match: {count}:{number}")print("nutrients:")for n in model.N:actual = sum(value(model.a[f,n] * model.x[f]) for f in model.F)print(f" {n}: actual value: {actual:.2f}; boundary:[{value(model.Nmin[n])},{value(model.Nmax[n])}]")
模型构建方式:
(1).AbstractModel:模型定义时不包含具体数据。
(2).ConcreteModel:模型定义时直接包含具体数据。
DataPortal类:加载数据到Pyomo模型,支持多种文件格式,如.dat, .json, .csv, Excel等。
建模组件:Set, Param, Var, Constraint, Objective等。
SolverFactory类:创建求解器实例,这里使用的是开源求解器glpk。
5.入口函数实现如下:
if __name__ == "__main__":colorama.init(autoreset=True)args = parse_args()start = time.perf_counter()main(args.file, args.number)end = time.perf_counter()print(f"elapsed time: {end-start:.2f} seconds")print(colorama.Fore.GREEN + "====== execution completed ======")
6.执行结果如下:与之前的结果一致
GitHub:https://github.com/fengbingchun/Python_Test