Skip to content

Commit 2a16b19

Browse files
authored
Merge pull request #3201 from jsiirola/mixed-standard-form
Add "mixed" standard form representation
2 parents a9b9112 + 8c4fb77 commit 2a16b19

File tree

2 files changed

+89
-15
lines changed

2 files changed

+89
-15
lines changed

Diff for: pyomo/repn/plugins/standard_form.py

+48-15
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,15 @@ class LinearStandardFormCompiler(object):
139139
description='Add slack variables and return `min cTx s.t. Ax == b`',
140140
),
141141
)
142+
CONFIG.declare(
143+
'mixed_form',
144+
ConfigValue(
145+
default=False,
146+
domain=bool,
147+
description='Return A in mixed form (the comparison operator is a '
148+
'mix of <=, ==, and >=)',
149+
),
150+
)
142151
CONFIG.declare(
143152
'show_section_timing',
144153
ConfigValue(
@@ -332,6 +341,9 @@ def write(self, model):
332341
# Tabulate constraints
333342
#
334343
slack_form = self.config.slack_form
344+
mixed_form = self.config.mixed_form
345+
if slack_form and mixed_form:
346+
raise ValueError("cannot specify both slack_form and mixed_form")
335347
rows = []
336348
rhs = []
337349
con_data = []
@@ -372,7 +384,30 @@ def write(self, model):
372384
f"model contains a trivially infeasible constraint, '{con.name}'"
373385
)
374386

375-
if slack_form:
387+
if mixed_form:
388+
N = len(repn.linear)
389+
_data = np.fromiter(repn.linear.values(), float, N)
390+
_index = np.fromiter(map(var_order.__getitem__, repn.linear), float, N)
391+
if ub == lb:
392+
rows.append(RowEntry(con, 0))
393+
rhs.append(ub - offset)
394+
con_data.append(_data)
395+
con_index.append(_index)
396+
con_index_ptr.append(con_index_ptr[-1] + N)
397+
else:
398+
if ub is not None:
399+
rows.append(RowEntry(con, 1))
400+
rhs.append(ub - offset)
401+
con_data.append(_data)
402+
con_index.append(_index)
403+
con_index_ptr.append(con_index_ptr[-1] + N)
404+
if lb is not None:
405+
rows.append(RowEntry(con, -1))
406+
rhs.append(lb - offset)
407+
con_data.append(_data)
408+
con_index.append(_index)
409+
con_index_ptr.append(con_index_ptr[-1] + N)
410+
elif slack_form:
376411
_data = list(repn.linear.values())
377412
_index = list(map(var_order.__getitem__, repn.linear))
378413
if lb == ub: # TODO: add tolerance?
@@ -437,24 +472,22 @@ def write(self, model):
437472
# at the index pointer list (an O(num_var) operation).
438473
c_ip = c.indptr
439474
A_ip = A.indptr
440-
active_var_idx = list(
441-
filter(
442-
lambda i: A_ip[i] != A_ip[i + 1] or c_ip[i] != c_ip[i + 1],
443-
range(len(columns)),
444-
)
445-
)
446-
nCol = len(active_var_idx)
475+
active_var_mask = (A_ip[1:] > A_ip[:-1]) | (c_ip[1:] > c_ip[:-1])
476+
477+
# Masks on NumPy arrays are very fast. Build the reduced A
478+
# indptr and then check if we actually have to manipulate the
479+
# columns
480+
augmented_mask = np.concatenate((active_var_mask, [True]))
481+
reduced_A_indptr = A.indptr[augmented_mask]
482+
nCol = len(reduced_A_indptr) - 1
447483
if nCol != len(columns):
448-
# Note that the indptr can't just use range() because a var
449-
# may only appear in the objectives or the constraints.
450-
columns = list(map(columns.__getitem__, active_var_idx))
451-
active_var_idx.append(c.indptr[-1])
484+
columns = [v for k, v in zip(active_var_mask, columns) if k]
452485
c = scipy.sparse.csc_array(
453-
(c.data, c.indices, c.indptr.take(active_var_idx)), [c.shape[0], nCol]
486+
(c.data, c.indices, c.indptr[augmented_mask]), [c.shape[0], nCol]
454487
)
455-
active_var_idx[-1] = A.indptr[-1]
488+
# active_var_idx[-1] = len(columns)
456489
A = scipy.sparse.csc_array(
457-
(A.data, A.indices, A.indptr.take(active_var_idx)), [A.shape[0], nCol]
490+
(A.data, A.indices, reduced_A_indptr), [A.shape[0], nCol]
458491
)
459492

460493
if self.config.nonnegative_vars:

Diff for: pyomo/repn/tests/test_standard_form.py

+41
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,23 @@ def test_linear_model(self):
4242
self.assertTrue(np.all(repn.c == np.array([0, 0, 0])))
4343
self.assertTrue(np.all(repn.A == np.array([[-1, -2, 0], [0, 1, 4]])))
4444
self.assertTrue(np.all(repn.rhs == np.array([-3, 5])))
45+
self.assertEqual(repn.rows, [(m.c, -1), (m.d, 1)])
46+
self.assertEqual(repn.columns, [m.x, m.y[1], m.y[3]])
47+
48+
def test_almost_dense_linear_model(self):
49+
m = pyo.ConcreteModel()
50+
m.x = pyo.Var()
51+
m.y = pyo.Var([1, 2, 3])
52+
m.c = pyo.Constraint(expr=m.x + 2 * m.y[1] + 4 * m.y[3] >= 10)
53+
m.d = pyo.Constraint(expr=5 * m.x + 6 * m.y[1] + 8 * m.y[3] <= 20)
54+
55+
repn = LinearStandardFormCompiler().write(m)
56+
57+
self.assertTrue(np.all(repn.c == np.array([0, 0, 0])))
58+
self.assertTrue(np.all(repn.A == np.array([[-1, -2, -4], [5, 6, 8]])))
59+
self.assertTrue(np.all(repn.rhs == np.array([-10, 20])))
60+
self.assertEqual(repn.rows, [(m.c, -1), (m.d, 1)])
61+
self.assertEqual(repn.columns, [m.x, m.y[1], m.y[3]])
4562

4663
def test_linear_model_row_col_order(self):
4764
m = pyo.ConcreteModel()
@@ -57,6 +74,8 @@ def test_linear_model_row_col_order(self):
5774
self.assertTrue(np.all(repn.c == np.array([0, 0, 0])))
5875
self.assertTrue(np.all(repn.A == np.array([[4, 0, 1], [0, -1, -2]])))
5976
self.assertTrue(np.all(repn.rhs == np.array([5, -3])))
77+
self.assertEqual(repn.rows, [(m.d, 1), (m.c, -1)])
78+
self.assertEqual(repn.columns, [m.y[3], m.x, m.y[1]])
6079

6180
def test_suffix_warning(self):
6281
m = pyo.ConcreteModel()
@@ -222,6 +241,28 @@ def test_alternative_forms(self):
222241
)
223242
self._verify_solution(soln, repn, True)
224243

244+
repn = LinearStandardFormCompiler().write(
245+
m, mixed_form=True, column_order=col_order
246+
)
247+
248+
self.assertEqual(
249+
repn.rows, [(m.c, -1), (m.d, 1), (m.e, 1), (m.e, -1), (m.f, 0)]
250+
)
251+
self.assertEqual(list(map(str, repn.x)), ['x', 'y[0]', 'y[1]', 'y[3]'])
252+
self.assertEqual(
253+
list(v.bounds for v in repn.x), [(None, None), (0, 10), (-5, 10), (-5, -2)]
254+
)
255+
ref = np.array(
256+
[[1, 0, 2, 0], [0, 0, 1, 4], [0, 1, 6, 0], [0, 1, 6, 0], [1, 1, 0, 0]]
257+
)
258+
self.assertTrue(np.all(repn.A == ref))
259+
self.assertTrue(np.all(repn.b == np.array([3, 5, 6, -3, 8])))
260+
self.assertTrue(np.all(repn.c == np.array([[-1, 0, -5, 0], [1, 0, 0, 15]])))
261+
# Note that the mixed_form solution is a mix of inequality and
262+
# equality constraints, so we cannot (easily) reuse the
263+
# _verify_solutions helper (as in the above cases):
264+
# self._verify_solution(soln, repn, False)
265+
225266
repn = LinearStandardFormCompiler().write(
226267
m, slack_form=True, nonnegative_vars=True, column_order=col_order
227268
)

0 commit comments

Comments
 (0)