Skip to content

Commit f520b2a

Browse files
authored
Merge pull request #3402 from emma58/lp-dual
Adding (parameterized) linear programming dual transformation!
2 parents 05c5ff2 + aa5e6d0 commit f520b2a

9 files changed

+1623
-18
lines changed

pyomo/contrib/fme/fourier_motzkin_elimination.py

+2-18
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from pyomo.repn.standard_repn import generate_standard_repn
3131
from pyomo.common.collections import ComponentMap, ComponentSet
3232
from pyomo.opt import TerminationCondition
33+
from pyomo.util.config_domains import ComponentDataSet
3334

3435
import logging
3536

@@ -57,23 +58,6 @@ def _check_var_bounds_filter(constraint):
5758
return True
5859

5960

60-
def vars_to_eliminate_list(x):
61-
if isinstance(x, (Var, VarData)):
62-
if not x.is_indexed():
63-
return ComponentSet([x])
64-
ans = ComponentSet()
65-
for j in x.index_set():
66-
ans.add(x[j])
67-
return ans
68-
elif hasattr(x, '__iter__'):
69-
ans = ComponentSet()
70-
for i in x:
71-
ans.update(vars_to_eliminate_list(i))
72-
return ans
73-
else:
74-
raise ValueError("Expected Var or list of Vars.\n\tReceived %s" % type(x))
75-
76-
7761
def gcd(a, b):
7862
while b != 0:
7963
a, b = b, a % b
@@ -111,7 +95,7 @@ class Fourier_Motzkin_Elimination_Transformation(Transformation):
11195
'vars_to_eliminate',
11296
ConfigValue(
11397
default=None,
114-
domain=vars_to_eliminate_list,
98+
domain=ComponentDataSet(Var),
11599
description="Continuous variable or list of continuous variables to "
116100
"project out of the model",
117101
doc="""

pyomo/core/plugins/transform/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,5 @@
2222
add_slack_vars,
2323
scaling,
2424
logical_to_linear,
25+
lp_dual,
2526
)
+260
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
# ___________________________________________________________________________
2+
#
3+
# Pyomo: Python Optimization Modeling Objects
4+
# Copyright (c) 2008-2024
5+
# National Technology and Engineering Solutions of Sandia, LLC
6+
# Under the terms of Contract DE-NA0003525 with National Technology and
7+
# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain
8+
# rights in this software.
9+
# This software is distributed under the 3-clause BSD License.
10+
# ___________________________________________________________________________
11+
12+
from pyomo.common.autoslots import AutoSlots
13+
from pyomo.common.collections import ComponentMap
14+
from pyomo.common.config import ConfigDict, ConfigValue
15+
from pyomo.common.dependencies import scipy
16+
from pyomo.core import (
17+
ConcreteModel,
18+
Block,
19+
Var,
20+
Constraint,
21+
Objective,
22+
TransformationFactory,
23+
NonNegativeReals,
24+
NonPositiveReals,
25+
maximize,
26+
minimize,
27+
Reals,
28+
)
29+
from pyomo.opt import WriterFactory
30+
from pyomo.repn.standard_repn import isclose_const
31+
from pyomo.util.config_domains import ComponentDataSet
32+
33+
34+
class _LPDualData(AutoSlots.Mixin):
35+
__slots__ = ('primal_var', 'dual_var', 'primal_constraint', 'dual_constraint')
36+
37+
def __init__(self):
38+
self.primal_var = {}
39+
self.dual_var = {}
40+
self.primal_constraint = ComponentMap()
41+
self.dual_constraint = ComponentMap()
42+
43+
44+
Block.register_private_data_initializer(_LPDualData)
45+
46+
47+
@TransformationFactory.register(
48+
'core.lp_dual', 'Generate the linear programming dual of the given model'
49+
)
50+
class LinearProgrammingDual(object):
51+
CONFIG = ConfigDict("core.lp_dual")
52+
CONFIG.declare(
53+
'parameterize_wrt',
54+
ConfigValue(
55+
default=None,
56+
domain=ComponentDataSet(Var),
57+
description="Vars to treat as data for the purposes of taking the dual",
58+
doc="""
59+
Optional list of Vars to be treated as data while taking the LP dual.
60+
61+
For example, if this is the dual of the inner problem in a multilevel
62+
optimization problem, then the outer problem's Vars would be specified
63+
in this list since they are not variables from the perspective of the
64+
inner problem.
65+
""",
66+
),
67+
)
68+
69+
def apply_to(self, model, **options):
70+
raise NotImplementedError(
71+
"The 'core.lp_dual' transformation does not implement "
72+
"apply_to since it is ambiguous what it means to take a dual "
73+
"in place. Please use 'create_using' and do what you wish with the "
74+
"returned model."
75+
)
76+
77+
def create_using(self, model, ostream=None, **kwds):
78+
"""Take linear programming dual of a model
79+
80+
Returns
81+
-------
82+
ConcreteModel containing linear programming dual
83+
84+
Parameters
85+
----------
86+
model: ConcreteModel
87+
The concrete Pyomo model to take the dual of
88+
89+
ostream: None
90+
This is provided for API compatibility with other writers
91+
and is ignored here.
92+
93+
"""
94+
config = self.CONFIG(kwds.pop('options', {}))
95+
config.set_value(kwds)
96+
97+
if config.parameterize_wrt is None:
98+
std_form = WriterFactory('compile_standard_form').write(
99+
model, mixed_form=True, set_sense=None
100+
)
101+
else:
102+
std_form = WriterFactory('compile_parameterized_standard_form').write(
103+
model, wrt=config.parameterize_wrt, mixed_form=True, set_sense=None
104+
)
105+
return self._take_dual(model, std_form)
106+
107+
def _take_dual(self, model, std_form):
108+
if len(std_form.objectives) != 1:
109+
raise ValueError(
110+
"Model '%s' has no objective or multiple active objectives. Can "
111+
"only take dual with exactly one active objective!" % model.name
112+
)
113+
primal_sense = std_form.objectives[0].sense
114+
115+
dual = ConcreteModel(name="%s dual" % model.name)
116+
# This is a csc matrix, so we'll skip transposing and just work off
117+
# of the columns
118+
A = std_form.A
119+
c = std_form.c.todense().ravel()
120+
dual_rows = range(A.shape[1])
121+
dual_cols = range(A.shape[0])
122+
dual.x = Var(dual_cols, domain=NonNegativeReals)
123+
trans_info = dual.private_data()
124+
for j, (primal_cons, ineq) in enumerate(std_form.rows):
125+
# maximize is -1 and minimize is +1 and ineq is +1 for <= and -1 for
126+
# >=, so we need to change domain to NonPositiveReals if the product
127+
# of these is +1.
128+
if primal_sense * ineq == 1:
129+
dual.x[j].domain = NonPositiveReals
130+
elif ineq == 0:
131+
# equality
132+
dual.x[j].domain = Reals
133+
trans_info.primal_constraint[dual.x[j]] = primal_cons
134+
trans_info.dual_var[primal_cons] = dual.x[j]
135+
136+
dual.constraints = Constraint(dual_rows)
137+
for i, primal in enumerate(std_form.columns):
138+
lhs = 0
139+
for j in range(A.indptr[i], A.indptr[i + 1]):
140+
coef = A.data[j]
141+
primal_row = A.indices[j]
142+
lhs += coef * dual.x[primal_row]
143+
144+
if primal.domain is Reals:
145+
dual.constraints[i] = lhs == c[i]
146+
elif primal_sense is minimize:
147+
if primal.domain is NonNegativeReals:
148+
dual.constraints[i] = lhs <= c[i]
149+
else: # primal.domain is NonPositiveReals
150+
dual.constraints[i] = lhs >= c[i]
151+
else:
152+
if primal.domain is NonNegativeReals:
153+
dual.constraints[i] = lhs >= c[i]
154+
else: # primal.domain is NonPositiveReals
155+
dual.constraints[i] = lhs <= c[i]
156+
trans_info.dual_constraint[primal] = dual.constraints[i]
157+
trans_info.primal_var[dual.constraints[i]] = primal
158+
159+
dual.obj = Objective(
160+
expr=sum(std_form.rhs[j] * dual.x[j] for j in dual_cols),
161+
sense=-primal_sense,
162+
)
163+
164+
return dual
165+
166+
def get_primal_constraint(self, model, dual_var):
167+
"""Return the primal constraint corresponding to 'dual_var'
168+
169+
Returns
170+
-------
171+
Constraint
172+
173+
Parameters
174+
----------
175+
model: ConcreteModel
176+
A dual model returned from the 'core.lp_dual' transformation
177+
dual_var: Var
178+
A dual variable on 'model'
179+
180+
"""
181+
primal_constraint = model.private_data().primal_constraint
182+
if dual_var in primal_constraint:
183+
return primal_constraint[dual_var]
184+
else:
185+
raise ValueError(
186+
"It does not appear that Var '%s' is a dual variable on model '%s'"
187+
% (dual_var.name, model.name)
188+
)
189+
190+
def get_dual_constraint(self, model, primal_var):
191+
"""Return the dual constraint corresponding to 'primal_var'
192+
193+
Returns
194+
-------
195+
Constraint
196+
197+
Parameters
198+
----------
199+
model: ConcreteModel
200+
A primal model passed as an argument to the 'core.lp_dual' transformation
201+
primal_var: Var
202+
A primal variable on 'model'
203+
204+
"""
205+
dual_constraint = model.private_data().dual_constraint
206+
if primal_var in dual_constraint:
207+
return dual_constraint[primal_var]
208+
else:
209+
raise ValueError(
210+
"It does not appear that Var '%s' is a primal variable on model '%s'"
211+
% (primal_var.name, model.name)
212+
)
213+
214+
def get_primal_var(self, model, dual_constraint):
215+
"""Return the primal variable corresponding to 'dual_constraint'
216+
217+
Returns
218+
-------
219+
Var
220+
221+
Parameters
222+
----------
223+
model: ConcreteModel
224+
A dual model returned from the 'core.lp_dual' transformation
225+
dual_constraint: Constraint
226+
A constraint on 'model'
227+
228+
"""
229+
primal_var = model.private_data().primal_var
230+
if dual_constraint in primal_var:
231+
return primal_var[dual_constraint]
232+
else:
233+
raise ValueError(
234+
"It does not appear that Constraint '%s' is a dual constraint on "
235+
"model '%s'" % (dual_constraint.name, model.name)
236+
)
237+
238+
def get_dual_var(self, model, primal_constraint):
239+
"""Return the dual variable corresponding to 'primal_constraint'
240+
241+
Returns
242+
-------
243+
Var
244+
245+
Parameters
246+
----------
247+
model: ConcreteModel
248+
A primal model passed as an argument to the 'core.lp_dual' transformation
249+
primal_constraint: Constraint
250+
A constraint on 'model'
251+
252+
"""
253+
dual_var = model.private_data().dual_var
254+
if primal_constraint in dual_var:
255+
return dual_var[primal_constraint]
256+
else:
257+
raise ValueError(
258+
"It does not appear that Constraint '%s' is a primal constraint on "
259+
"model '%s'" % (primal_constraint.name, model.name)
260+
)

0 commit comments

Comments
 (0)