后缀

后缀提供了一种声明额外模型数据的机制,这些数据可以在多种上下文中使用。最常见的是,求解器插件使用后缀来存储关于模型解决方案的额外信息。通过使用Suffix组件类,建模者可以使用这些和其他后缀功能。Suffix的用途包括:

  • 从求解器中导入关于数学程序解决方案的额外信息(例如,约束对偶、变量减少成本、基础信息)。

  • 将信息导出到求解器或算法以帮助解决数学程序(例如,热启动信息,变量分支优先级)。

  • 为建模组件标记本地数据,以便在高级脚本算法中后续使用。

后缀表示法和Pyomo NL文件接口

Pyomo中使用的Suffix组件是从建模语言AMPL中使用的后缀符号改编而来的[FGK02]。因此,自然地,使用Pyomo的NL文件接口可以完全使用AMPL风格的后缀功能。有关AMPL风格后缀的信息,读者可以参考AMPL网站:

许多脚本示例展示了AMPL风格后缀功能的使用,这些示例可以在与Pyomo一起分发的examples/pyomo/suffixes目录中找到。

声明

在Pyomo模型上声明Suffix组件的影响由以下特性决定:

  • 方向:此特性定义了信息流的方向对于后缀。后缀方向可以被赋予以下四种可能的值之一:

    • LOCAL - 后缀数据保留在建模框架本地,不会由求解器插件导入或导出(默认)

    • IMPORT - 后缀数据将由各自的求解器插件从求解器中导入

    • EXPORT - 后缀数据将通过其各自的求解器插件导出到求解器

    • IMPORT_EXPORT - 后缀数据在模型与求解器或算法之间双向流动

  • 数据类型:此特性在那些重要的接口(例如,NL文件接口)上宣传后缀上持有的数据类型。后缀数据类型可以被赋予以下三种可能的值之一:

    • FLOAT - 后缀存储浮点数据(默认)

    • INT - 后缀存储整数数据

    • None - 后缀存储任何类型的数据

注意

通过Pyomo的NL文件接口导出后缀数据要求所有活动的导出后缀具有严格的数据类型(即,不允许datatype=None)。

以下代码片段展示了在Pyomo模型上声明Suffix组件的示例:

import pyomo.environ as pyo

model = pyo.ConcreteModel()

# Export integer data
model.priority = pyo.Suffix(
    direction=pyo.Suffix.EXPORT, datatype=pyo.Suffix.INT)

# Export and import floating point data
model.dual = pyo.Suffix(direction=pyo.Suffix.IMPORT_EXPORT)

# Store floating point data
model.junk = pyo.Suffix()

在模型上声明具有非局部方向的Suffix并不能保证与Pyomo中的所有求解器插件兼容。给定的Suffix是否可接受取决于所使用的求解器和求解器接口。在某些情况下,如果求解器插件遇到它不处理的Suffix类型,它将引发异常,但这并非在所有情况下都成立。例如,NL文件接口适用于所有与AMPL兼容的求解器,因此无法验证具有给定名称、方向和数据类型的Suffix是否适合某个求解器。在切换到不同的求解器或求解器接口时,应小心验证Suffix声明是否按预期处理。

操作

Suffix组件类提供了一个字典接口,用于将Pyomo建模组件映射到任意数据。这种映射功能被封装在ComponentMap基类中,该基类也可以在Pyomo的建模环境中使用。在需要从Pyomo建模组件到任意数据值的简单映射的情况下,ComponentMap可以作为Suffix的更轻量级替代品。

注意

ComponentMap 和 Suffix 使用内置的 id() 函数来哈希条目键。这一设计决策源于以下事实:在 Pyomo 中发现的大多数建模组件要么不可哈希,要么使用基于可变数值的哈希,这使得它们无法作为内置 dict 类的键使用。

警告

在ComponentMap和Suffix中使用内置的id()函数来哈希条目键,使得它们不适合在必须使用内置对象类型作为键的情况下使用。强烈建议在这些映射容器中仅使用Pyomo建模组件作为键(VarConstraint等)。

警告

不要尝试对ComponentMap或Suffix的实例进行pickle或deepcopy操作,除非同时对其所持有的映射条目的组件进行这些操作。例如,将这些对象放置在模型上,然后克隆或pickle该模型是一个可接受的场景。

除了通过ComponentMap基类提供的字典接口外,Suffix组件类还提供了许多方法,这些方法的默认语义更适合处理索引建模组件。通过一个示例来突出显示此功能是最简单的方式。

model = pyo.ConcreteModel()
model.x = pyo.Var()
model.y = pyo.Var([1,2,3])
model.foo = pyo.Suffix()

在这个例子中,我们有一个具体的Pyomo模型,包含两种不同类型的变量组件(索引和非索引)以及一个后缀声明(foo)。接下来的代码片段展示了如何向后缀foo添加条目的示例。

# Assign a suffix value of 1.0 to model.x
model.foo.set_value(model.x, 1.0)

# Same as above with dict interface
model.foo[model.x] = 1.0

# Assign a suffix value of 0.0 to all indices of model.y
# By default this expands so that entries are created for
# every index (y[1], y[2], y[3]) and not model.y itself
model.foo.set_value(model.y, 0.0)

# The same operation using the dict interface results in an entry only
# for the parent component model.y
model.foo[model.y] = 50.0

# Assign a suffix value of -1.0 to model.y[1]
model.foo.set_value(model.y[1], -1.0)

# Same as above with the dict interface
model.foo[model.y[1]] = -1.0

在这个例子中,我们强调了__setitem__setValue入口方法可以互换使用,除了在使用索引组件的情况下(model.y)。在索引的情况下, __setitem__方法为父索引组件本身创建一个单独的条目,而setValue方法默认情况下为组件的每个索引创建一个条目。这种行为可以通过使用可选关键字‘expand’来控制,将其赋值为False会导致与__setitem__相同的行为。

其他操作,如访问或删除我们映射中的条目,可以像使用内置的dict类一样执行。

>>> print(model.foo.get(model.x))
1.0
>>> print(model.foo[model.x])
1.0

>>> print(model.foo.get(model.y[1]))
-1.0
>>> print(model.foo[model.y[1]])
-1.0

>>> print(model.foo.get(model.y[2]))
0.0
>>> print(model.foo[model.y[2]])
0.0

>>> print(model.foo.get(model.y))
50.0
>>> print(model.foo[model.y])
50.0

>>> del model.foo[model.y]
>>> print(model.foo.get(model.y))
None

>>> print(model.foo[model.y])
Traceback (most recent call last):
  ...
KeyError: "Component with id '...': y"

非字典方法 clear_value 可以用来代替 __delitem__ 来移除条目,它继承了与 setValue 相同的默认行为,用于索引组件,并且当参数在映射中不作为键存在时不会引发 KeyError。

>>> model.foo.clear_value(model.y)

>>> print(model.foo[model.y[1]])
Traceback (most recent call last):
  ...
KeyError: "Component with id '...': y[1]"

>>> del model.foo[model.y[1]]
Traceback (most recent call last):
  ...
KeyError: "Component with id '...': y[1]"

>>> model.foo.clear_value(model.y[1])

这里提供了一个非字典后缀方法的摘要:

clearAllValues()
Clears all suffix data.

clear_value(component, expand=True)
Clears suffix information for a component.

setAllValues(value)
Sets the value of this suffix on all components.

setValue(component, value, expand=True)
Sets the value of this suffix on the specified component.

updateValues(data_buffer, expand=True)
Updates the suffix data given a list of component,value tuples. Provides
an improvement in efficiency over calling setValue on every component.

getDatatype()
Return the suffix datatype.

setDatatype(datatype)
Set the suffix datatype.

getDirection()
Return the suffix direction.

setDirection(direction)
Set the suffix direction.

importEnabled()
Returns True when this suffix is enabled for import from solutions.

exportEnabled()
Returns True when this suffix is enabled for export to solvers.

导入后缀数据

从求解器解决方案中导入后缀信息是通过声明一个具有适当名称和方向的后缀组件来实现的。可用于导入的后缀名称可能特定于第三方求解器以及Pyomo中的各个求解器接口。其中最常见的是约束对偶乘数,大多数求解器和求解器接口都支持。请求将对偶导入后缀数据可以通过在模型上声明一个后缀组件来完成。

model = pyo.ConcreteModel()
model.dual = pyo.Suffix(direction=pyo.Suffix.IMPORT)
model.x = pyo.Var()
model.obj = pyo.Objective(expr=model.x)
model.con = pyo.Constraint(expr=model.x >= 1.0)

存在一个名为dual的活动后缀,其具有导入样式后缀方向,将导致约束对偶信息被收集到求解器结果中(假设求解器提供对偶信息)。除此之外,在将求解器结果加载到问题实例中后(使用python脚本或与pyomo命令结合的Pyomo回调函数),可以使用dual后缀组件访问与约束相关的对偶值。

>>> results = pyo.SolverFactory('glpk').solve(model)
>>> pyo.assert_optimal_termination(results)
>>> print(model.dual[model.con])
1.0

或者,可以使用pyomo选项--solver-suffixes从求解器请求后缀信息。如果通过此命令行选项提供了后缀名称,pyomo脚本将自动在构建的实例上声明这些后缀组件,从而使这些后缀可用于导入。

导出后缀数据

导出后缀数据的方式与导入后缀数据类似。只需在模型上声明一个具有导出样式后缀方向的Suffix组件,并将建模组件值与之关联。以下示例展示了如何使用AMPL风格的后缀符号与Pyomo的NL文件接口结合来声明一个类型1的特殊有序集。

model = pyo.ConcreteModel()
model.y = pyo.Var([1,2,3], within=pyo.NonNegativeReals)

model.sosno = pyo.Suffix(direction=pyo.Suffix.EXPORT)
model.ref = pyo.Suffix(direction=pyo.Suffix.EXPORT)

# Add entry for each index of model.y
model.sosno.set_value(model.y, 1)
model.ref[model.y[1]] = 0
model.ref[model.y[2]] = 1
model.ref[model.y[3]] = 2

大多数与AMPL兼容的求解器会识别后缀名称sosnoref作为声明一个特殊有序集合,其中sosno的正值 表示类型1的特殊有序集合,负值表示类型2的特殊有序集合。

注意

Pyomo 提供了 SOSConstraint 组件用于声明特殊有序集,该组件被所有求解器接口识别,包括 NL 文件接口。

Pyomo的NL文件接口将识别名为'dual'的EXPORT样式Suffix组件,作为约束乘子的初始化提供。因此,它将与NL编写器中遇到的所有其他EXPORT样式后缀分开处理,这些后缀被视为AMPL样式的后缀。以下示例脚本展示了如何通过提供原始(变量值)和对偶(后缀)解信息来热启动内点求解器Ipopt。这个对偶后缀信息可以使用具有IMPORT_EXPORT方向的单个Suffix组件进行导入和导出。

model = pyo.ConcreteModel()
model.x1 = pyo.Var(bounds=(1,5),initialize=1.0)
model.x2 = pyo.Var(bounds=(1,5),initialize=5.0)
model.x3 = pyo.Var(bounds=(1,5),initialize=5.0)
model.x4 = pyo.Var(bounds=(1,5),initialize=1.0)
model.obj = pyo.Objective(
    expr=model.x1*model.x4*(model.x1 + model.x2 + model.x3) + model.x3)
model.inequality = pyo.Constraint(
    expr=model.x1*model.x2*model.x3*model.x4 >= 25.0)
model.equality = pyo.Constraint(
    expr=model.x1**2 + model.x2**2 + model.x3**2 + model.x4**2 == 40.0)

### Declare all suffixes
# Ipopt bound multipliers (obtained from solution)
model.ipopt_zL_out = pyo.Suffix(direction=pyo.Suffix.IMPORT)
model.ipopt_zU_out = pyo.Suffix(direction=pyo.Suffix.IMPORT)
# Ipopt bound multipliers (sent to solver)
model.ipopt_zL_in = pyo.Suffix(direction=pyo.Suffix.EXPORT)
model.ipopt_zU_in = pyo.Suffix(direction=pyo.Suffix.EXPORT)
# Obtain dual solutions from first solve and send to warm start
model.dual = pyo.Suffix(direction=pyo.Suffix.IMPORT_EXPORT)

ipopt = pyo.SolverFactory('ipopt')

通过检查Ipopt的迭代日志,可以看到有和没有热启动时的性能差异:

  • 没有预热启动:

    ipopt.solve(model, tee=True)
    
    ...
    iter    objective    inf_pr   inf_du lg(mu)  ||d||  lg(rg) alpha_du alpha_pr  ls
       0  1.6109693e+01 1.12e+01 5.28e-01  -1.0 0.00e+00    -  0.00e+00 0.00e+00   0
       1  1.6982239e+01 7.30e-01 1.02e+01  -1.0 6.11e-01    -  7.19e-02 1.00e+00f  1
       2  1.7318411e+01 ...
       ...
       8  1.7014017e+01 ...
    
    Number of Iterations....: 8
    ...
    
  • 使用热启动:

    ### Set Ipopt options for warm-start
    # The current values on the ipopt_zU_out and ipopt_zL_out suffixes will
    # be used as initial conditions for the bound multipliers to solve the
    # new problem
    model.ipopt_zL_in.update(model.ipopt_zL_out)
    model.ipopt_zU_in.update(model.ipopt_zU_out)
    ipopt.options['warm_start_init_point'] = 'yes'
    ipopt.options['warm_start_bound_push'] = 1e-6
    ipopt.options['warm_start_mult_bound_push'] = 1e-6
    ipopt.options['mu_init'] = 1e-6
    
    ipopt.solve(model, tee=True)
    
    ...
    iter    objective    inf_pr   inf_du lg(mu)  ||d||  lg(rg) alpha_du alpha_pr  ls
       0  1.7014032e+01 2.00e-06 4.07e-06  -6.0 0.00e+00    -  0.00e+00 0.00e+00   0
       1  1.7014019e+01 3.65e-12 1.00e-11  -6.0 2.50e-01    -  1.00e+00 1.00e+00h  1
       2  1.7014017e+01 ...
    
    Number of Iterations....: 2
    ...
    

在AbstractModel中使用后缀

为了允许在AbstractModel框架内声明后缀数据,可以使用可选的构造规则初始化Suffix组件。与约束规则一样,此函数将在模型构建时执行。以下简单示例突出了在后缀初始化中使用rule关键字的情况。后缀规则预期返回一个(组件, 值)元组的可迭代对象,其中对于索引组件应用了expand=True语义。

model = pyo.AbstractModel()
model.x = pyo.Var()
model.c = pyo.Constraint(expr=model.x >= 1)

def foo_rule(m):
   return ((m.x, 2.0), (m.c, 3.0))
model.foo = pyo.Suffix(rule=foo_rule)
>>> # Instantiate the model
>>> inst = model.create_instance()

>>> print(inst.foo[inst.x])
2.0
>>> print(inst.foo[inst.c])
3.0

>>> # Note that model.x and inst.x are not the same object
>>> print(inst.foo[model.x])
Traceback (most recent call last):
  ...
KeyError: "Component with id '...': x"

下一个示例展示了一个抽象模型,其中后缀仅附加到变量上:

model = pyo.AbstractModel()
model.I = pyo.RangeSet(1,4)
model.x = pyo.Var(model.I)
def c_rule(m, i):
    return m.x[i] >= i
model.c = pyo.Constraint(model.I, rule=c_rule)

def foo_rule(m):
    return ((m.x[i], 3.0*i) for i in m.I)
model.foo = pyo.Suffix(rule=foo_rule)
>>> # instantiate the model
>>> inst = model.create_instance()
>>> for i in inst.I:
...     print((i, inst.foo[inst.x[i]]))
(1, 3.0)
(2, 6.0)
(3, 9.0)
(4, 12.0)