From 927c46c660189526e4728c8c0b37fcf82ae94bce Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 18 Mar 2024 12:25:11 -0600 Subject: [PATCH 1/5] Add 'mixed' option to standard form writer --- pyomo/repn/plugins/standard_form.py | 37 ++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/pyomo/repn/plugins/standard_form.py b/pyomo/repn/plugins/standard_form.py index 239cd845930..d0e1014d549 100644 --- a/pyomo/repn/plugins/standard_form.py +++ b/pyomo/repn/plugins/standard_form.py @@ -139,6 +139,15 @@ class LinearStandardFormCompiler(object): description='Add slack variables and return `min cTx s.t. Ax == b`', ), ) + CONFIG.declare( + 'mixed_form', + ConfigValue( + default=False, + domain=bool, + description='Return A in mixed form (the comparison operator is a ' + 'mix of <=, ==, and >=)', + ), + ) CONFIG.declare( 'show_section_timing', ConfigValue( @@ -332,6 +341,9 @@ def write(self, model): # Tabulate constraints # slack_form = self.config.slack_form + mixed_form = self.config.mixed_form + if slack_form and mixed_form: + raise ValueError("cannot specify both slack_form and mixed_form") rows = [] rhs = [] con_data = [] @@ -372,7 +384,30 @@ def write(self, model): f"model contains a trivially infeasible constraint, '{con.name}'" ) - if slack_form: + if mixed_form: + N = len(repn.linear) + _data = np.fromiter(repn.linear.values(), float, N) + _index = np.fromiter(map(var_order.__getitem__, repn.linear), float, N) + if ub == lb: + rows.append(RowEntry(con, 0)) + rhs.append(ub - offset) + con_data.append(_data) + con_index.append(_index) + con_index_ptr.append(con_index_ptr[-1] + N) + else: + if ub is not None: + rows.append(RowEntry(con, 1)) + rhs.append(ub - offset) + con_data.append(_data) + con_index.append(_index) + con_index_ptr.append(con_index_ptr[-1] + N) + if lb is not None: + rows.append(RowEntry(con, -1)) + rhs.append(lb - offset) + con_data.append(_data) + con_index.append(_index) + con_index_ptr.append(con_index_ptr[-1] + N) + elif slack_form: _data = list(repn.linear.values()) _index = list(map(var_order.__getitem__, repn.linear)) if lb == ub: # TODO: add tolerance? From 64211e187f5a3daa2d0d1c4c4061aae4866c4b44 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Mon, 18 Mar 2024 12:25:37 -0600 Subject: [PATCH 2/5] Fix error when removing unused variables --- pyomo/repn/plugins/standard_form.py | 26 ++++++++++++-------------- pyomo/repn/tests/test_standard_form.py | 13 +++++++++++++ 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/pyomo/repn/plugins/standard_form.py b/pyomo/repn/plugins/standard_form.py index d0e1014d549..ea7b6a6a9e6 100644 --- a/pyomo/repn/plugins/standard_form.py +++ b/pyomo/repn/plugins/standard_form.py @@ -472,24 +472,22 @@ def write(self, model): # at the index pointer list (an O(num_var) operation). c_ip = c.indptr A_ip = A.indptr - active_var_idx = list( - filter( - lambda i: A_ip[i] != A_ip[i + 1] or c_ip[i] != c_ip[i + 1], - range(len(columns)), - ) - ) - nCol = len(active_var_idx) + active_var_mask = (A_ip[1:] > A_ip[:-1]) | (c_ip[1:] > c_ip[:-1]) + + # Masks on NumPy arrays are very fast. Build the reduced A + # indptr and then check if we actually have to manipulate the + # columns + augmented_mask = np.concatenate((active_var_mask, [True])) + reduced_A_indptr = A.indptr[augmented_mask] + nCol = len(reduced_A_indptr) - 1 if nCol != len(columns): - # Note that the indptr can't just use range() because a var - # may only appear in the objectives or the constraints. - columns = list(map(columns.__getitem__, active_var_idx)) - active_var_idx.append(c.indptr[-1]) + columns = [v for k, v in zip(active_var_mask, columns) if k] c = scipy.sparse.csc_array( - (c.data, c.indices, c.indptr.take(active_var_idx)), [c.shape[0], nCol] + (c.data, c.indices, c.indptr[augmented_mask]), [c.shape[0], nCol] ) - active_var_idx[-1] = A.indptr[-1] + # active_var_idx[-1] = len(columns) A = scipy.sparse.csc_array( - (A.data, A.indices, A.indptr.take(active_var_idx)), [A.shape[0], nCol] + (A.data, A.indices, reduced_A_indptr), [A.shape[0], nCol] ) if self.config.nonnegative_vars: diff --git a/pyomo/repn/tests/test_standard_form.py b/pyomo/repn/tests/test_standard_form.py index e24195edfde..c8b914deca5 100644 --- a/pyomo/repn/tests/test_standard_form.py +++ b/pyomo/repn/tests/test_standard_form.py @@ -43,6 +43,19 @@ def test_linear_model(self): self.assertTrue(np.all(repn.A == np.array([[-1, -2, 0], [0, 1, 4]]))) self.assertTrue(np.all(repn.rhs == np.array([-3, 5]))) + def test_almost_dense_linear_model(self): + m = pyo.ConcreteModel() + m.x = pyo.Var() + m.y = pyo.Var([1, 2, 3]) + m.c = pyo.Constraint(expr=m.x + 2 * m.y[1] + 4 * m.y[3] >= 10) + m.d = pyo.Constraint(expr=5 * m.x + 6 * m.y[1] + 8 * m.y[3] <= 20) + + repn = LinearStandardFormCompiler().write(m) + + self.assertTrue(np.all(repn.c == np.array([0, 0, 0]))) + self.assertTrue(np.all(repn.A == np.array([[-1, -2, -4], [5, 6, 8]]))) + self.assertTrue(np.all(repn.rhs == np.array([-10, 20]))) + def test_linear_model_row_col_order(self): m = pyo.ConcreteModel() m.x = pyo.Var() From 56b513dcf2ec852ce993400d3245b58cb46c5fc4 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 15:38:29 -0600 Subject: [PATCH 3/5] add tests for mixed standard form --- pyomo/repn/tests/test_standard_form.py | 40 ++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/pyomo/repn/tests/test_standard_form.py b/pyomo/repn/tests/test_standard_form.py index c8b914deca5..9dee2b1d25d 100644 --- a/pyomo/repn/tests/test_standard_form.py +++ b/pyomo/repn/tests/test_standard_form.py @@ -42,6 +42,8 @@ def test_linear_model(self): self.assertTrue(np.all(repn.c == np.array([0, 0, 0]))) self.assertTrue(np.all(repn.A == np.array([[-1, -2, 0], [0, 1, 4]]))) self.assertTrue(np.all(repn.rhs == np.array([-3, 5]))) + self.assertEqual(repn.rows, [(m.c, -1), (m.d, 1)]) + self.assertEqual(repn.columns, [m.x, m.y[1], m.y[3]]) def test_almost_dense_linear_model(self): m = pyo.ConcreteModel() @@ -55,6 +57,8 @@ def test_almost_dense_linear_model(self): self.assertTrue(np.all(repn.c == np.array([0, 0, 0]))) self.assertTrue(np.all(repn.A == np.array([[-1, -2, -4], [5, 6, 8]]))) self.assertTrue(np.all(repn.rhs == np.array([-10, 20]))) + self.assertEqual(repn.rows, [(m.c, -1), (m.d, 1)]) + self.assertEqual(repn.columns, [m.x, m.y[1], m.y[3]]) def test_linear_model_row_col_order(self): m = pyo.ConcreteModel() @@ -70,6 +74,8 @@ def test_linear_model_row_col_order(self): self.assertTrue(np.all(repn.c == np.array([0, 0, 0]))) self.assertTrue(np.all(repn.A == np.array([[4, 0, 1], [0, -1, -2]]))) self.assertTrue(np.all(repn.rhs == np.array([5, -3]))) + self.assertEqual(repn.rows, [(m.d, 1), (m.c, -1)]) + self.assertEqual(repn.columns, [m.y[3], m.x, m.y[1]]) def test_suffix_warning(self): m = pyo.ConcreteModel() @@ -235,6 +241,40 @@ def test_alternative_forms(self): ) self._verify_solution(soln, repn, True) + repn = LinearStandardFormCompiler().write( + m, mixed_form=True, column_order=col_order + ) + + self.assertEqual(repn.rows, [(m.c, -1), (m.d, 1), (m.e, 1), (m.e, -1), (m.f, 0)]) + self.assertEqual( + list(map(str, repn.x)), + ['x', 'y[0]', 'y[1]', 'y[3]'], + ) + self.assertEqual( + list(v.bounds for v in repn.x), + [(None, None), (0, 10), (-5, 10), (-5, -2)], + ) + ref = np.array( + [ + [1, 0, 2, 0], + [0, 0, 1, 4], + [0, 1, 6, 0], + [0, 1, 6, 0], + [1, 1, 0, 0], + ] + ) + self.assertTrue(np.all(repn.A == ref)) + print(repn) + print(repn.b) + self.assertTrue(np.all(repn.b == np.array([3, 5, 6, -3, 8]))) + self.assertTrue( + np.all( + repn.c == np.array([[-1, 0, -5, 0], [1, 0, 0, 15]]) + ) + ) + # Note that the solution is a mix of inequality and equality constraints + # self._verify_solution(soln, repn, False) + repn = LinearStandardFormCompiler().write( m, slack_form=True, nonnegative_vars=True, column_order=col_order ) From 2f1d4a0387995b741028f96e0e05aadc8c4a30a0 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Wed, 20 Mar 2024 20:44:43 -0600 Subject: [PATCH 4/5] NFC: apply black --- pyomo/repn/tests/test_standard_form.py | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/pyomo/repn/tests/test_standard_form.py b/pyomo/repn/tests/test_standard_form.py index 9dee2b1d25d..591703e6ae8 100644 --- a/pyomo/repn/tests/test_standard_form.py +++ b/pyomo/repn/tests/test_standard_form.py @@ -245,33 +245,21 @@ def test_alternative_forms(self): m, mixed_form=True, column_order=col_order ) - self.assertEqual(repn.rows, [(m.c, -1), (m.d, 1), (m.e, 1), (m.e, -1), (m.f, 0)]) self.assertEqual( - list(map(str, repn.x)), - ['x', 'y[0]', 'y[1]', 'y[3]'], + repn.rows, [(m.c, -1), (m.d, 1), (m.e, 1), (m.e, -1), (m.f, 0)] ) + self.assertEqual(list(map(str, repn.x)), ['x', 'y[0]', 'y[1]', 'y[3]']) self.assertEqual( - list(v.bounds for v in repn.x), - [(None, None), (0, 10), (-5, 10), (-5, -2)], + list(v.bounds for v in repn.x), [(None, None), (0, 10), (-5, 10), (-5, -2)] ) ref = np.array( - [ - [1, 0, 2, 0], - [0, 0, 1, 4], - [0, 1, 6, 0], - [0, 1, 6, 0], - [1, 1, 0, 0], - ] + [[1, 0, 2, 0], [0, 0, 1, 4], [0, 1, 6, 0], [0, 1, 6, 0], [1, 1, 0, 0]] ) self.assertTrue(np.all(repn.A == ref)) print(repn) print(repn.b) self.assertTrue(np.all(repn.b == np.array([3, 5, 6, -3, 8]))) - self.assertTrue( - np.all( - repn.c == np.array([[-1, 0, -5, 0], [1, 0, 0, 15]]) - ) - ) + self.assertTrue(np.all(repn.c == np.array([[-1, 0, -5, 0], [1, 0, 0, 15]]))) # Note that the solution is a mix of inequality and equality constraints # self._verify_solution(soln, repn, False) From 8c4fb774a30a621e7890dd006cc9e83931c545e6 Mon Sep 17 00:00:00 2001 From: John Siirola Date: Sun, 31 Mar 2024 20:17:52 -0600 Subject: [PATCH 5/5] Remove debugging; expand comment explaining difference in cut-and-paste tests --- pyomo/repn/tests/test_standard_form.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/repn/tests/test_standard_form.py b/pyomo/repn/tests/test_standard_form.py index 591703e6ae8..4c66ae87c41 100644 --- a/pyomo/repn/tests/test_standard_form.py +++ b/pyomo/repn/tests/test_standard_form.py @@ -256,11 +256,11 @@ def test_alternative_forms(self): [[1, 0, 2, 0], [0, 0, 1, 4], [0, 1, 6, 0], [0, 1, 6, 0], [1, 1, 0, 0]] ) self.assertTrue(np.all(repn.A == ref)) - print(repn) - print(repn.b) self.assertTrue(np.all(repn.b == np.array([3, 5, 6, -3, 8]))) self.assertTrue(np.all(repn.c == np.array([[-1, 0, -5, 0], [1, 0, 0, 15]]))) - # Note that the solution is a mix of inequality and equality constraints + # Note that the mixed_form solution is a mix of inequality and + # equality constraints, so we cannot (easily) reuse the + # _verify_solutions helper (as in the above cases): # self._verify_solution(soln, repn, False) repn = LinearStandardFormCompiler().write(