Skip to content

Commit 5dbc835

Browse files
authored
Merge pull request #3678 from mrmundt/lpwriter_bug
Bugfix: lp_writer SOS constraints and row_order
2 parents 9a7de1b + d7c0595 commit 5dbc835

4 files changed

Lines changed: 240 additions & 28 deletions

File tree

examples/pyomo/sos/sos2_piecewise.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222

2323
model = pyo.ConcreteModel()
2424

25-
model.index_set = pyo.Set(initialize=[1, 2])
25+
model.idx_set = pyo.Set(initialize=[1, 2])
2626
DOMAIN_PTS = {1: [1, 2, 3], 2: [1, 2, 3]}
2727
F = {1: [1, 4, 9], 2: [1, 4, 9]}
2828
# Note we can also implement this like below
@@ -37,18 +37,18 @@ def SOS_indices_init(model, t):
3737

3838

3939
model.SOS_indices = pyo.Set(
40-
model.index_set, dimen=2, ordered=True, initialize=SOS_indices_init
40+
model.idx_set, dimen=2, ordered=True, initialize=SOS_indices_init
4141
)
4242

4343

4444
def sos_var_indices_init(model):
45-
return [(t, i) for t in model.index_set for i in range(len(DOMAIN_PTS[t]))]
45+
return [(t, i) for t in model.idx_set for i in range(len(DOMAIN_PTS[t]))]
4646

4747

4848
model.sos_var_indices = pyo.Set(ordered=True, dimen=2, initialize=sos_var_indices_init)
4949

50-
model.x = pyo.Var(model.index_set) # domain variable
51-
model.Fx = pyo.Var(model.index_set) # range variable
50+
model.x = pyo.Var(model.idx_set) # domain variable
51+
model.Fx = pyo.Var(model.idx_set) # range variable
5252
model.y = pyo.Var(model.sos_var_indices, within=pyo.NonNegativeReals) # SOS2 variable
5353

5454
model.obj = pyo.Objective(expr=pyo.sum_product(model.Fx), sense=pyo.maximize)
@@ -73,11 +73,11 @@ def constraint3_rule(model, t):
7373
return sum(model.y[t, j] for j in range(len(DOMAIN_PTS[t]))) == 1
7474

7575

76-
model.constraint1 = pyo.Constraint(model.index_set, rule=constraint1_rule)
77-
model.constraint2 = pyo.Constraint(model.index_set, rule=constraint2_rule)
78-
model.constraint3 = pyo.Constraint(model.index_set, rule=constraint3_rule)
76+
model.constraint1 = pyo.Constraint(model.idx_set, rule=constraint1_rule)
77+
model.constraint2 = pyo.Constraint(model.idx_set, rule=constraint2_rule)
78+
model.constraint3 = pyo.Constraint(model.idx_set, rule=constraint3_rule)
7979
model.SOS_set_constraint = pyo.SOSConstraint(
80-
model.index_set, var=model.y, index=model.SOS_indices, sos=2
80+
model.idx_set, var=model.y, index=model.SOS_indices, sos=2
8181
)
8282

8383
# Fix the answer for testing purposes

pyomo/repn/plugins/lp_writer.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
InEnum,
2020
document_kwargs_from_configdict,
2121
)
22+
from pyomo.common.deprecation import deprecation_warning
2223
from pyomo.common.gc_manager import PauseGC
2324
from pyomo.common.timing import TicTocTimer
2425

@@ -30,12 +31,10 @@
3031
Param,
3132
Expression,
3233
SOSConstraint,
33-
SortComponents,
3434
Suffix,
3535
SymbolMap,
3636
minimize,
3737
)
38-
from pyomo.core.base.component import ActiveComponent
3938
from pyomo.core.base.label import LPFileLabeler, NumericLabeler
4039
from pyomo.opt import WriterFactory
4140
from pyomo.repn.linear import LinearRepnVisitor
@@ -48,6 +47,7 @@
4847
initialize_var_map_from_column_order,
4948
int_float,
5049
ordered_active_constraints,
50+
row_order2row_map,
5151
)
5252

5353
### FIXME: Remove the following as soon as non-active components no
@@ -555,18 +555,16 @@ def write(self, model):
555555
)
556556
)
557557
if self.config.row_order:
558-
# sort() is stable (per Python docs), so we can let
559-
# all unspecified rows have a row number one bigger than
560-
# the number of rows specified by the user ordering.
561-
_n = len(row_order)
562-
sos.sort(key=lambda x: _row_getter(x, _n))
558+
row_map = row_order2row_map(self.config)
559+
_n = len(row_map)
560+
sos.sort(key=lambda x: row_map.get(id(x), _n))
563561

564562
ostream.write("\nSOS\n")
565563
for soscon in sos:
566564
ostream.write(f'\n{getSymbol(soscon)}: S{soscon.level}::\n')
567565
for v, w in getattr(soscon, 'get_items', soscon.items)():
568566
if w.__class__ not in int_float:
569-
w = float(f)
567+
w = float(w)
570568
ostream.write(f" {getSymbol(v)}:{w!s}\n")
571569

572570
ostream.write("\nend\n")

pyomo/repn/tests/cpxlp/test_lpv2.py

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,34 @@
2020
from pyomo.repn.plugins.lp_writer import LPWriter
2121

2222

23+
def create_sos_model():
24+
m = pyo.ConcreteModel()
25+
26+
m.IDX = pyo.Set(initialize=[1, 2])
27+
m.SOS2_VARS = pyo.Set(m.IDX, initialize={1: [1, 2, 3], 2: [4, 5, 6]})
28+
m.SOS3_VARS = pyo.Set(m.IDX, initialize={1: [1, 3, 5], 2: [2, 4, 6]})
29+
30+
m.x = pyo.Var(pyo.RangeSet(6))
31+
32+
m.sos1 = pyo.SOSConstraint(var=m.x, sos=2)
33+
m.sos2 = pyo.SOSConstraint(m.IDX, var=m.x, index=m.SOS2_VARS, sos=1)
34+
m.sos3 = pyo.SOSConstraint(m.IDX, var=m.x, index=m.SOS3_VARS, sos=2)
35+
36+
@m.Constraint()
37+
def con1(m, i):
38+
return sum(m.x[i] for i in m.x) == 0
39+
40+
@m.Constraint(m.IDX)
41+
def con2(m, i):
42+
return m.x[i] >= 20 + i
43+
44+
@m.Constraint(m.IDX)
45+
def con3(m, i):
46+
return m.x[i] >= 30 + i
47+
48+
return m
49+
50+
2351
class TestLPv2(unittest.TestCase):
2452
def test_warn_export_suffixes(self):
2553
m = pyo.ConcreteModel()
@@ -139,3 +167,181 @@ def test_deterministic_unordered_sets(self):
139167
self.assertEqual(LOG.getvalue(), "")
140168

141169
self.assertEqual(ref, OUT.getvalue())
170+
171+
def test_SOS_ordering(self):
172+
# This is in response to a bug that was identified with
173+
# config.row_order with SOS constraints. If an SOS constraint
174+
# was added and the `config.row_order` option selected,
175+
# bug would ensue.
176+
177+
model = create_sos_model()
178+
179+
OUT = StringIO()
180+
LPWriter().write(model, OUT, symbolic_solver_labels=True)
181+
self.assertEqual(
182+
r"""\* Source Pyomo model name=unknown *\
183+
184+
min
185+
ScalarObjective:
186+
+1.0 ONE_VAR_CONSTANT
187+
188+
s.t.
189+
190+
c_e_con1_:
191+
+1 x(1)
192+
+1 x(2)
193+
+1 x(3)
194+
+1 x(4)
195+
+1 x(5)
196+
+1 x(6)
197+
= 0
198+
199+
c_l_con2(1)_:
200+
+1 x(1)
201+
>= 21
202+
203+
c_l_con2(2)_:
204+
+1 x(2)
205+
>= 22
206+
207+
c_l_con3(1)_:
208+
+1 x(1)
209+
>= 31
210+
211+
c_l_con3(2)_:
212+
+1 x(2)
213+
>= 32
214+
215+
bounds
216+
1 <= ONE_VAR_CONSTANT <= 1
217+
-inf <= x(1) <= +inf
218+
-inf <= x(2) <= +inf
219+
-inf <= x(3) <= +inf
220+
-inf <= x(4) <= +inf
221+
-inf <= x(5) <= +inf
222+
-inf <= x(6) <= +inf
223+
SOS
224+
225+
sos1: S2::
226+
x(1):1
227+
x(2):2
228+
x(3):3
229+
x(4):4
230+
x(5):5
231+
x(6):6
232+
233+
sos2(1): S1::
234+
x(1):1
235+
x(2):2
236+
x(3):3
237+
238+
sos2(2): S1::
239+
x(4):1
240+
x(5):2
241+
x(6):3
242+
243+
sos3(1): S2::
244+
x(1):1
245+
x(3):2
246+
x(5):3
247+
248+
sos3(2): S2::
249+
x(2):1
250+
x(4):2
251+
x(6):3
252+
253+
end
254+
""",
255+
OUT.getvalue(),
256+
)
257+
258+
OUT = StringIO()
259+
LPWriter().write(
260+
model,
261+
OUT,
262+
symbolic_solver_labels=True,
263+
row_order=[
264+
model.sos3[2],
265+
model.con3[2],
266+
model.con2,
267+
model.sos2,
268+
model.sos1,
269+
model.con1,
270+
],
271+
)
272+
self.assertEqual(
273+
r"""\* Source Pyomo model name=unknown *\
274+
275+
min
276+
ScalarObjective:
277+
+1.0 ONE_VAR_CONSTANT
278+
279+
s.t.
280+
281+
c_l_con3(2)_:
282+
+1 x(2)
283+
>= 32
284+
285+
c_l_con2(1)_:
286+
+1 x(1)
287+
>= 21
288+
289+
c_l_con2(2)_:
290+
+1 x(2)
291+
>= 22
292+
293+
c_e_con1_:
294+
+1 x(1)
295+
+1 x(2)
296+
+1 x(3)
297+
+1 x(4)
298+
+1 x(5)
299+
+1 x(6)
300+
= 0
301+
302+
c_l_con3(1)_:
303+
+1 x(1)
304+
>= 31
305+
306+
bounds
307+
1 <= ONE_VAR_CONSTANT <= 1
308+
-inf <= x(1) <= +inf
309+
-inf <= x(2) <= +inf
310+
-inf <= x(3) <= +inf
311+
-inf <= x(4) <= +inf
312+
-inf <= x(5) <= +inf
313+
-inf <= x(6) <= +inf
314+
SOS
315+
316+
sos3(2): S2::
317+
x(2):1
318+
x(4):2
319+
x(6):3
320+
321+
sos2(1): S1::
322+
x(1):1
323+
x(2):2
324+
x(3):3
325+
326+
sos2(2): S1::
327+
x(4):1
328+
x(5):2
329+
x(6):3
330+
331+
sos1: S2::
332+
x(1):1
333+
x(2):2
334+
x(3):3
335+
x(4):4
336+
x(5):5
337+
x(6):6
338+
339+
sos3(1): S2::
340+
x(1):1
341+
x(3):2
342+
x(5):3
343+
344+
end
345+
""",
346+
OUT.getvalue(),
347+
)

pyomo/repn/util.py

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -748,17 +748,17 @@ def initialize_var_map_from_column_order(model, config, var_map):
748748
return var_map
749749

750750

751-
def ordered_active_constraints(model, config):
751+
def row_order2row_map(config):
752+
"""Convert a row_order (list or ComponentMap) into a dict mapping
753+
constraint id -> row index. Returns an empty dict if no ordering."""
754+
row_order = config.row_order
755+
if row_order is None or isinstance(row_order, bool):
756+
return {}
757+
752758
sorter = FileDeterminism_to_SortComponents(config.file_determinism)
753-
constraints = model.component_data_objects(Constraint, active=True, sort=sorter)
754759

755-
row_order = config.row_order
756-
if row_order is None or row_order.__class__ is bool:
757-
return constraints
758-
elif isinstance(row_order, ComponentMap):
759-
# The row order has historically also supported a ComponentMap of
760-
# component to position in addition to the simple list of
761-
# components. Convert it to the simple list
760+
if isinstance(row_order, ComponentMap):
761+
# Convert ComponentMap to sorted list based on its values
762762
row_order = sorted(row_order, key=row_order.__getitem__)
763763

764764
row_map = {}
@@ -768,10 +768,18 @@ def ordered_active_constraints(model, config):
768768
row_map[id(c)] = c
769769
else:
770770
row_map[id(con)] = con
771+
772+
# Map the implicit dict ordering to an explicit 0..n ordering
773+
return {_id: i for i, _id in enumerate(row_map)}
774+
775+
776+
def ordered_active_constraints(model, config):
777+
sorter = FileDeterminism_to_SortComponents(config.file_determinism)
778+
constraints = model.component_data_objects(Constraint, active=True, sort=sorter)
779+
780+
row_map = row_order2row_map(config)
771781
if not row_map:
772782
return constraints
773-
# map the implicit dict ordering to an explicit 0..n ordering
774-
row_map = {_id: i for i, _id in enumerate(row_map)}
775783
# sorted() is stable (per Python docs), so we can let all
776784
# unspecified rows have a row number one bigger than the
777785
# number of rows specified by the user ordering.

0 commit comments

Comments
 (0)