Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Require variables and constraints to be specified separately in IncidenceGraphInterface.remove_nodes #3212

Merged
merged 9 commits into from
Apr 9, 2024
55 changes: 43 additions & 12 deletions pyomo/contrib/incidence_analysis/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -453,11 +453,29 @@
raise ValueError("Neither variables nor a model have been provided.")
else:
variables = self.variables
elif self._incidence_graph is not None:
# If variables were provided and an incidence graph is cached,
# make sure the provided variables exist in the graph.
for var in variables:
if var not in self._var_index_map:
raise KeyError(
f"Variable {var} does not exist in the cached"
" incidence graph."
)
if constraints is None:
if self._incidence_graph is None:
raise ValueError("Neither constraints nor a model have been provided.")
else:
constraints = self.constraints
elif self._incidence_graph is not None:
# If constraints were provided and an incidence graph is cached,
# make sure the provided constraints exist in the graph.
for con in constraints:
if con not in self._con_index_map:
raise KeyError(

Check warning on line 475 in pyomo/contrib/incidence_analysis/interface.py

View check run for this annotation

Codecov / codecov/patch

pyomo/contrib/incidence_analysis/interface.py#L475

Added line #L475 was not covered by tests
f"Constraint {con} does not exist in the cached"
" incidence graph."
)

_check_unindexed(variables + constraints)
return variables, constraints
Expand Down Expand Up @@ -854,7 +872,7 @@
# Hopefully this does not get too confusing...
return var_partition, con_partition

def remove_nodes(self, nodes, constraints=None):
def remove_nodes(self, variables=None, constraints=None):
"""Removes the specified variables and constraints (columns and
rows) from the cached incidence matrix.

Expand All @@ -866,35 +884,48 @@

Parameters
----------
nodes: list
VarData or ConData objects whose columns or rows will be
removed from the incidence matrix.
variables: list
VarData objects whose nodes will be removed from the incidence graph
constraints: list
VarData or ConData objects whose columns or rows will be
removed from the incidence matrix.
ConData objects whose nodes will be removed from the incidence graph

.. note::

**Breaking change in Pyomo vTBD**
mrmundt marked this conversation as resolved.
Show resolved Hide resolved

The pre-TBD implementation of ``remove_nodes`` allowed variables and
constraints to remove to be specified in a single list. This made
error checking difficult, and indeed, if invalid components were
provided, we carried on silently instead of throwing an error or
warning. As part of a fix to raise an error if an invalid component
(one that is not part of the incidence graph) is provided, we now require
variables and constraints to be specified separately.

"""
if constraints is None:
constraints = []
if variables is None:
variables = []

Check warning on line 908 in pyomo/contrib/incidence_analysis/interface.py

View check run for this annotation

Codecov / codecov/patch

pyomo/contrib/incidence_analysis/interface.py#L908

Added line #L908 was not covered by tests
if self._incidence_graph is None:
raise RuntimeError(
"Attempting to remove variables and constraints from cached "
"incidence matrix,\nbut no incidence matrix has been cached."
)
to_exclude = ComponentSet(nodes)
to_exclude.update(constraints)
vars_to_include = [v for v in self.variables if v not in to_exclude]
cons_to_include = [c for c in self.constraints if c not in to_exclude]
variables, constraints = self._validate_input(variables, constraints)
v_exclude = ComponentSet(variables)
c_exclude = ComponentSet(constraints)
vars_to_include = [v for v in self.variables if v not in v_exclude]
cons_to_include = [c for c in self.constraints if c not in c_exclude]
incidence_graph = self._extract_subgraph(vars_to_include, cons_to_include)
# update attributes
self._variables = vars_to_include
self._constraints = cons_to_include
self._incidence_graph = incidence_graph
self._var_index_map = ComponentMap(
(var, i) for i, var in enumerate(self.variables)
(var, i) for i, var in enumerate(vars_to_include)
)
self._con_index_map = ComponentMap(
(con, i) for i, con in enumerate(self._constraints)
(con, i) for i, con in enumerate(cons_to_include)
)

def plot(self, variables=None, constraints=None, title=None, show=True):
Expand Down
33 changes: 22 additions & 11 deletions pyomo/contrib/incidence_analysis/tests/test_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -634,17 +634,15 @@ def test_exception(self):
nlp = PyomoNLP(model)
igraph = IncidenceGraphInterface(nlp)

with self.assertRaises(RuntimeError) as exc:
with self.assertRaisesRegex(KeyError, "does not exist"):
variables = [model.P]
constraints = [model.ideal_gas]
igraph.maximum_matching(variables, constraints)
self.assertIn("must be unindexed", str(exc.exception))

with self.assertRaises(RuntimeError) as exc:
with self.assertRaisesRegex(KeyError, "does not exist"):
variables = [model.P]
constraints = [model.ideal_gas]
igraph.block_triangularize(variables, constraints)
self.assertIn("must be unindexed", str(exc.exception))


@unittest.skipUnless(networkx_available, "networkx is not available.")
Expand Down Expand Up @@ -885,17 +883,15 @@ def test_exception(self):
model = make_gas_expansion_model()
igraph = IncidenceGraphInterface(model)

with self.assertRaises(RuntimeError) as exc:
with self.assertRaisesRegex(KeyError, "does not exist"):
variables = [model.P]
constraints = [model.ideal_gas]
igraph.maximum_matching(variables, constraints)
self.assertIn("must be unindexed", str(exc.exception))

with self.assertRaises(RuntimeError) as exc:
with self.assertRaisesRegex(KeyError, "does not exist"):
variables = [model.P]
constraints = [model.ideal_gas]
igraph.block_triangularize(variables, constraints)
self.assertIn("must be unindexed", str(exc.exception))

@unittest.skipUnless(scipy_available, "scipy is not available.")
def test_remove(self):
Expand Down Expand Up @@ -923,7 +919,7 @@ def test_remove(self):
# Say we know that these variables and constraints should
# be matched...
vars_to_remove = [model.F[0], model.F[2]]
cons_to_remove = (model.mbal[1], model.mbal[2])
cons_to_remove = [model.mbal[1], model.mbal[2]]
igraph.remove_nodes(vars_to_remove, cons_to_remove)
variable_set = ComponentSet(igraph.variables)
self.assertNotIn(model.F[0], variable_set)
Expand Down Expand Up @@ -1309,7 +1305,7 @@ def test_remove(self):
# matrix.
vars_to_remove = [m.flow_comp[1]]
cons_to_remove = [m.flow_eqn[1]]
igraph.remove_nodes(vars_to_remove + cons_to_remove)
igraph.remove_nodes(vars_to_remove, cons_to_remove)
var_dmp, con_dmp = igraph.dulmage_mendelsohn()
var_con_set = ComponentSet(igraph.variables + igraph.constraints)
underconstrained_set = ComponentSet(
Expand Down Expand Up @@ -1460,6 +1456,21 @@ def test_remove_no_matrix(self):
with self.assertRaisesRegex(RuntimeError, "no incidence matrix"):
igraph.remove_nodes([m.v1])

def test_remove_bad_node(self):
m = pyo.ConcreteModel()
m.x = pyo.Var([1, 2, 3])
m.eq = pyo.Constraint(pyo.PositiveIntegers)
m.eq[1] = m.x[1] * m.x[2] == m.x[3]
m.eq[2] = m.x[1] + 2 * m.x[2] == 3 * m.x[3]
igraph = IncidenceGraphInterface(m)
with self.assertRaisesRegex(KeyError, "does not exist"):
# Suppose we think something like this should work. We should get
# an error, and not silently do nothing.
igraph.remove_nodes([m.x], [m.eq])

with self.assertRaisesRegex(KeyError, "does not exist"):
igraph.remove_nodes([[m.x[1], m.x[2]], [m.eq[1]]])


@unittest.skipUnless(networkx_available, "networkx is not available.")
@unittest.skipUnless(scipy_available, "scipy is not available.")
Expand Down Expand Up @@ -1840,7 +1851,7 @@ def test_var_elim(self):
for adj_con in igraph.get_adjacent_to(m.x[1]):
for adj_var in igraph.get_adjacent_to(m.eq4):
igraph.add_edge(adj_var, adj_con)
igraph.remove_nodes([m.x[1], m.eq4])
igraph.remove_nodes([m.x[1]], [m.eq4])

assert ComponentSet(igraph.variables) == ComponentSet([m.x[2], m.x[3], m.x[4]])
assert ComponentSet(igraph.constraints) == ComponentSet([m.eq1, m.eq2, m.eq3])
Expand Down