如何在 PuLP 中导入和导出模型

导出模型在建筑时间过长或需要将模型传递到另一台计算机进行求解时非常有用。或者出于任何其他原因。PuLP 提供了两种导出模型的方法:导出为 mps 文件或导出为字典/json 文件。每种方法都有其优势。

mps 格式 是一个行业标准。但它不是很灵活,因此一些信息无法存储。它只存储变量和约束。它不存储变量的值。

字典/ json 格式 是为了适应 pulp 存储信息的方式而设计的,因此它不会丢失信息:这种格式的文件保存了足够的数据,以便在读取时能够恢复完整的 pulp 模型。

两种格式的导入和导出接口是相似的,如下面的示例1所示。

考虑因素

需要考虑以下因素:

  1. 变量名需要是唯一的。PuLP 允许使用变量名,因为它为每个变量使用了一个内部代码。但我们不导出该代码。因此,我们仅通过名称来识别变量。

  2. 变量不是以分组的方式导出的。这意味着如果你有多个 每个包含许多变量的字典,你最终会得到一个非常长的变量列表。这在示例2中可以看到。

  3. 输出信息也会写入json格式。这意味着状态、解决方案状态、变量的值以及影子价格/降低成本也会被导出。这意味着可以导出一个已解决的模型,然后再次读取它以查看变量的值。

  4. 对于json,我们使用基础的 json 包。但如果 ujson 可用,我们会使用它,以便导入/导出可以非常快。

示例 1: json

一个取自内部测试的非常简单的例子。想象以下问题:

from pulp import *

prob = LpProblem("test_export_dict_MIP", LpMinimize)
x = LpVariable("x", 0, 4)
y = LpVariable("y", -1, 1)
z = LpVariable("z", 0, None, LpInteger)
prob += x + 4 * y + 9 * z, "obj"
prob += x + y <= 5, "c1"
prob += x + z >= 10, "c2"
prob += -y + z == 7.5, "c3"

我们现在可以将问题导出为字典:

data = prob.to_dict()

我们现在有一个包含大量数据的字典:

{'constraints': [{'coefficients': [{'name': 'x', 'value': 1},
                                   {'name': 'y', 'value': 1}],
                  'constant': -5,
                  'name': 'c1',
                  'pi': None,
                  'sense': -1},
                 {'coefficients': [{'name': 'x', 'value': 1},
                                   {'name': 'z', 'value': 1}],
                  'constant': -10,
                  'name': 'c2',
                  'pi': None,
                  'sense': 1},
                 {'coefficients': [{'name': 'y', 'value': -1},
                                   {'name': 'z', 'value': 1}],
                  'constant': -7.5,
                  'name': 'c3',
                  'pi': None,
                  'sense': 0}],
 'objective': {'coefficients': [{'name': 'x', 'value': 1},
                                {'name': 'y', 'value': 4},
                                {'name': 'z', 'value': 9}],
               'name': 'obj'},
 'parameters': {'name': 'test_export_dict_MIP',
                'sense': 1,
                'sol_status': 0,
                'status': 0},
 'sos1': {},
 'sos2': {},
 'variables': [{'cat': 'Continuous',
                'dj': None,
                'lowBound': 0,
                'name': 'x',
                'upBound': 4,
                'varValue': None},
               {'cat': 'Continuous',
                'dj': None,
                'lowBound': -1,
                'name': 'y',
                'upBound': 1,
                'varValue': None},
               {'cat': 'Integer',
                'dj': None,
                'lowBound': 0,
                'name': 'z',
                'upBound': None,
                'varValue': None}]}

我们现在可以导入这个字典:

var1, prob1 = LpProblem.from_dict(data)
var1
# {'x': x, 'y': y, 'z': z}
prob1
# test_export_dict_MIP:
# MINIMIZE
# 1*x + 4*y + 9*z + 0
# SUBJECT TO
# c1: x + y <= 5
# c2: x + z >= 10
# c3: - y + z = 7.5
# VARIABLES
# x <= 4 Continuous
# -1 <= y <= 1 Continuous
# 0 <= z Integer

如你所见,我们得到一个包含两个元素的元组:(1) 一个变量字典和 (2) 一个 PuLP 模型对象。我们现在可以解决这个问题:

prob1.solve()

结果将可在我们的 变量中获得:

var1['x'].value()
# 3.0

示例 1: mps

相同的模型:

from pulp import *
prob = LpProblem("test_export_dict_MIP", LpMinimize)
x = LpVariable("x", 0, 4)
y = LpVariable("y", -1, 1)
z = LpVariable("z", 0, None, LpInteger)
prob += x + 4 * y + 9 * z, "obj"
prob += x + y <= 5, "c1"
prob += x + z >= 10, "c2"
prob += -y + z == 7.5, "c3"

我们现在可以将问题导出为 mps 文件:

prob.writeMPS("test.mps")

我们现在可以导入这个文件:

var1, prob1 = LpProblem.fromMPS("test.mps")
var1
# {'x': x, 'y': y, 'z': z}
prob1
# test_export_dict_MIP:
# MINIMIZE
# 1*x + 4*y + 9*z + 0
# SUBJECT TO
# c1: x + y <= 5
# c2: x + z >= 10
# c3: - y + z = 7.5
# VARIABLES
# x <= 4 Continuous
# -1 <= y <= 1 Continuous
# 0 <= z Integer

生成的元组与之前的格式完全相同。

示例 2: json

我们将以 集合划分问题 中的模型为例:

import pulp

max_tables = 5
max_table_size = 4
guests = 'A B C D E F G I J K L M N O P Q R'.split()

def happiness(table):
    """
    Find the happiness of the table
    - by calculating the maximum distance between the letters
    """
    return abs(ord(table[0]) - ord(table[-1]))

#create list of all possible tables
possible_tables = [tuple(c) for c in pulp.allcombinations(guests,
                                        max_table_size)]

#create a binary variable to state that a table setting is used
x = pulp.LpVariable.dicts('table', possible_tables,
                            lowBound = 0,
                            upBound = 1,
                            cat = pulp.LpInteger)

seating_model = pulp.LpProblem("Wedding_Seating_Model", pulp.LpMinimize)

seating_model += pulp.lpSum([happiness(table) * x[table] for table in possible_tables])

#specify the maximum number of tables
seating_model += pulp.lpSum([x[table] for table in possible_tables]) <= max_tables, \
                            "Maximum_number_of_tables"

#A guest must seated at one and only one table
for guest in guests:
    seating_model += pulp.lpSum([x[table] for table in possible_tables
                                if guest in table]) == 1, "Must_seat_%s"%guest

我们可以*直接*通过以下方式解决模型:

seating_model.solve()

相反,我们将把它导出到一个json文件:

seating_model.to_json("seating_model.json")

并重新导入它:

wedding_vars, wedding_model = LpProblem.from_json("seating_model.json")

我们检查变量:

wedding_vars
{"table_('A',)": table_('A',), "table_('A',_'B')": table_('A',_'B'), "table_('A',_'B',_'C')": table_('A',_'B',_'C'), "table_('A',_'B',_'C',_'D')": table_('A',_'B',_'C',_'D'), "table_('A',_'B',_'C',_'E')": table_('A',_'B',_'C',_'E'), ...}

如所见,它不再是一个由原始元组索引的字典。不幸的是,它变成了一个带有连接名称的扁平字典。

我们仍然可以解决这个模型,尽管:

wedding_model.solve()

并检查一些值:

wedding_vars["table_('M',_'N')"].value()
# 1.0

分组变量

正如“注意事项”部分所述,变量的分组不会自动恢复。然而,通过在变量名称上使用一些严格的命名约定和巧妙的解析,可以重建变量的原始结构。

关于json和pandas / numpy数据类型的注意事项

python 中的 json 模块在转换 numpy 数据类型(例如 np.integer)时存在一些问题。解决这个问题的简单方法是提供一个自定义的编码类,如 这里 所示:

import numpy as np
#(...)
class NpEncoder(json.JSONEncoder):
    def default(self, obj):
        if isinstance(obj, np.integer):
            return int(obj)
        elif isinstance(obj, np.floating):
            return float(obj)
        elif isinstance(obj, np.ndarray):
            return obj.tolist()
        else:
            return super(NpEncoder, self).default(obj)

wedding_model.to_json("seating_model.json", cls=NpEncoder)

请注意,这个自定义编码类可能无法与 ujson 包一起使用。一个替代方案是在使用 pulp 之前,使用 int()float() 对所有值进行类型转换。