Skip to content

Commit f3cd7b8

Browse files
author
Clara Witte
committed
Merge branch 'maingo_interface' of https://github.com/MAiNGO-github/pyomo into maingo_interface
2 parents 55b7dff + 986e290 commit f3cd7b8

38 files changed

+989
-384
lines changed

.github/workflows/test_branches.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ jobs:
9292
skip_doctest: 1
9393
TARGET: linux
9494
PYENV: conda
95-
PACKAGES: mpi4py
95+
PACKAGES: mpi4py==3.1.5
9696

9797
- os: ubuntu-latest
9898
python: '3.10'

.github/workflows/test_pr_and_main.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ jobs:
9393
skip_doctest: 1
9494
TARGET: linux
9595
PYENV: conda
96-
PACKAGES: mpi4py
96+
PACKAGES: mpi4py==3.1.5
9797

9898
- os: ubuntu-latest
9999
python: '3.11'

doc/OnlineDocs/developer_reference/solvers.rst

+31
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,37 @@ be used with other Pyomo tools / capabilities.
8484
...
8585
3 Declarations: x y obj
8686

87+
In keeping with our commitment to backwards compatibility, both the legacy and
88+
future methods of specifying solver options are supported:
89+
90+
.. testcode::
91+
:skipif: not ipopt_available
92+
93+
import pyomo.environ as pyo
94+
95+
model = pyo.ConcreteModel()
96+
model.x = pyo.Var(initialize=1.5)
97+
model.y = pyo.Var(initialize=1.5)
98+
99+
def rosenbrock(model):
100+
return (1.0 - model.x) ** 2 + 100.0 * (model.y - model.x**2) ** 2
101+
102+
model.obj = pyo.Objective(rule=rosenbrock, sense=pyo.minimize)
103+
104+
# Backwards compatible
105+
status = pyo.SolverFactory('ipopt_v2').solve(model, options={'max_iter' : 6})
106+
# Forwards compatible
107+
status = pyo.SolverFactory('ipopt_v2').solve(model, solver_options={'max_iter' : 6})
108+
model.pprint()
109+
110+
.. testoutput::
111+
:skipif: not ipopt_available
112+
:hide:
113+
114+
2 Var Declarations
115+
...
116+
3 Declarations: x y obj
117+
87118
Using the new interfaces directly
88119
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
89120

pyomo/contrib/appsi/solvers/maingo.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -420,7 +420,7 @@ def _postsolve(self, timer: HierarchicalTimer):
420420
if status == maingopy.FEASIBLE_POINT:
421421
results.globally_optimal = False
422422
logger.warning(
423-
"MAiNGO did only find a feasible solution but did not prove its global optimality."
423+
"MAiNGO found a feasible solution but did not prove its global optimality."
424424
)
425425
elif status == maingopy.INFEASIBLE:
426426
results.termination_condition = TerminationCondition.infeasible

pyomo/contrib/gdpopt/tests/test_LBB.py

+1
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ def test_infeasible_GDP(self):
5959
self.assertIsNone(m.d.disjuncts[0].indicator_var.value)
6060
self.assertIsNone(m.d.disjuncts[1].indicator_var.value)
6161

62+
@unittest.skipUnless(z3_available, "Z3 SAT solver is not available")
6263
def test_infeasible_GDP_check_sat(self):
6364
"""Test for infeasible GDP with check_sat option True."""
6465
m = ConcreteModel()

pyomo/contrib/gdpopt/tests/test_gdpopt.py

+14-2
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
from pyomo.common.collections import Bunch
2323
from pyomo.common.config import ConfigDict, ConfigValue
2424
from pyomo.common.fileutils import import_file, PYOMO_ROOT_DIR
25-
from pyomo.contrib.appsi.solvers.gurobi import Gurobi
2625
from pyomo.contrib.gdpopt.create_oa_subproblems import (
2726
add_util_block,
2827
add_disjunct_list,
@@ -767,6 +766,9 @@ def test_time_limit(self):
767766
results.solver.termination_condition, TerminationCondition.maxTimeLimit
768767
)
769768

769+
@unittest.skipUnless(
770+
license_available, "No BARON license--8PP logical problem exceeds demo size"
771+
)
770772
def test_LOA_8PP_logical_default_init(self):
771773
"""Test logic-based outer approximation with 8PP."""
772774
exfile = import_file(join(exdir, 'eight_process', 'eight_proc_logical.py'))
@@ -870,6 +872,9 @@ def test_LOA_8PP_maxBinary(self):
870872
)
871873
ct.check_8PP_solution(self, eight_process, results)
872874

875+
@unittest.skipUnless(
876+
license_available, "No BARON license--8PP logical problem exceeds demo size"
877+
)
873878
def test_LOA_8PP_logical_maxBinary(self):
874879
"""Test logic-based OA with max_binary initialization."""
875880
exfile = import_file(join(exdir, 'eight_process', 'eight_proc_logical.py'))
@@ -1050,7 +1055,11 @@ def assert_correct_disjuncts_active(
10501055

10511056
self.assertTrue(fabs(value(eight_process.profit.expr) - 68) <= 1e-2)
10521057

1053-
@unittest.skipUnless(Gurobi().available(), "APPSI Gurobi solver is not available")
1058+
@unittest.skipUnless(
1059+
SolverFactory('appsi_gurobi').available(exception_flag=False)
1060+
and SolverFactory('appsi_gurobi').license_is_valid(),
1061+
"Legacy APPSI Gurobi solver is not available",
1062+
)
10541063
def test_auto_persistent_solver(self):
10551064
exfile = import_file(join(exdir, 'eight_process', 'eight_proc_model.py'))
10561065
m = exfile.build_eight_process_flowsheet()
@@ -1126,6 +1135,9 @@ def test_RIC_8PP_default_init(self):
11261135
)
11271136
ct.check_8PP_solution(self, eight_process, results)
11281137

1138+
@unittest.skipUnless(
1139+
license_available, "No BARON license--8PP logical problem exceeds demo size"
1140+
)
11291141
def test_RIC_8PP_logical_default_init(self):
11301142
"""Test logic-based outer approximation with 8PP."""
11311143
exfile = import_file(join(exdir, 'eight_process', 'eight_proc_logical.py'))

pyomo/contrib/gdpopt/util.py

+8-1
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,13 @@ def _add_bigm_constraint_to_transformed_model(m, constraint, block):
553553
# making a Reference to the ComponentData so that it will look like an
554554
# indexed component for now. If I redesign bigm at some point, then this
555555
# could be prettier.
556-
bigm._transform_constraint(Reference(constraint), parent_disjunct, None, [], [])
556+
bigm._transform_constraint(
557+
Reference(constraint),
558+
parent_disjunct,
559+
None,
560+
[],
561+
[],
562+
1 - parent_disjunct.binary_indicator_var,
563+
)
557564
# Now get rid of it because this is a class attribute!
558565
del bigm._config

pyomo/contrib/incidence_analysis/interface.py

+72-13
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
scipy as sp,
2929
plotly,
3030
)
31-
from pyomo.common.deprecation import deprecated
31+
from pyomo.common.deprecation import deprecated, deprecation_warning
3232
from pyomo.contrib.incidence_analysis.config import get_config_from_kwds
3333
from pyomo.contrib.incidence_analysis.matching import maximum_matching
3434
from pyomo.contrib.incidence_analysis.connected import get_independent_submatrices
@@ -453,11 +453,29 @@ def _validate_input(self, variables, constraints):
453453
raise ValueError("Neither variables nor a model have been provided.")
454454
else:
455455
variables = self.variables
456+
elif self._incidence_graph is not None:
457+
# If variables were provided and an incidence graph is cached,
458+
# make sure the provided variables exist in the graph.
459+
for var in variables:
460+
if var not in self._var_index_map:
461+
raise KeyError(
462+
f"Variable {var} does not exist in the cached"
463+
" incidence graph."
464+
)
456465
if constraints is None:
457466
if self._incidence_graph is None:
458467
raise ValueError("Neither constraints nor a model have been provided.")
459468
else:
460469
constraints = self.constraints
470+
elif self._incidence_graph is not None:
471+
# If constraints were provided and an incidence graph is cached,
472+
# make sure the provided constraints exist in the graph.
473+
for con in constraints:
474+
if con not in self._con_index_map:
475+
raise KeyError(
476+
f"Constraint {con} does not exist in the cached"
477+
" incidence graph."
478+
)
461479

462480
_check_unindexed(variables + constraints)
463481
return variables, constraints
@@ -854,7 +872,7 @@ def dulmage_mendelsohn(self, variables=None, constraints=None):
854872
# Hopefully this does not get too confusing...
855873
return var_partition, con_partition
856874

857-
def remove_nodes(self, nodes, constraints=None):
875+
def remove_nodes(self, variables=None, constraints=None):
858876
"""Removes the specified variables and constraints (columns and
859877
rows) from the cached incidence matrix.
860878
@@ -866,35 +884,76 @@ def remove_nodes(self, nodes, constraints=None):
866884
867885
Parameters
868886
----------
869-
nodes: list
870-
VarData or ConData objects whose columns or rows will be
871-
removed from the incidence matrix.
887+
variables: list
888+
VarData objects whose nodes will be removed from the incidence graph
872889
constraints: list
873-
VarData or ConData objects whose columns or rows will be
874-
removed from the incidence matrix.
890+
ConData objects whose nodes will be removed from the incidence graph
891+
892+
.. note::
893+
894+
**Deprecation in Pyomo v6.7.2.dev0**
895+
896+
The pre-6.7.2.dev0 implementation of ``remove_nodes`` allowed variables and
897+
constraints to remove to be specified in a single list. This made
898+
error checking difficult, and indeed, if invalid components were
899+
provided, we carried on silently instead of throwing an error or
900+
warning. As part of a fix to raise an error if an invalid component
901+
(one that is not part of the incidence graph) is provided, we now require
902+
variables and constraints to be specified separately.
875903
876904
"""
877905
if constraints is None:
878906
constraints = []
907+
if variables is None:
908+
variables = []
879909
if self._incidence_graph is None:
880910
raise RuntimeError(
881911
"Attempting to remove variables and constraints from cached "
882912
"incidence matrix,\nbut no incidence matrix has been cached."
883913
)
884-
to_exclude = ComponentSet(nodes)
885-
to_exclude.update(constraints)
886-
vars_to_include = [v for v in self.variables if v not in to_exclude]
887-
cons_to_include = [c for c in self.constraints if c not in to_exclude]
914+
915+
vars_to_validate = []
916+
cons_to_validate = []
917+
depr_msg = (
918+
"In IncidenceGraphInterface.remove_nodes, passing variables and"
919+
" constraints in the same list is deprecated. Please separate your"
920+
" variables and constraints and pass them in the order variables,"
921+
" constraints."
922+
)
923+
if any(var in self._con_index_map for var in variables) or any(
924+
con in self._var_index_map for con in constraints
925+
):
926+
deprecation_warning(depr_msg, version="6.7.2.dev0")
927+
# If we received variables/constraints in the same list, sort them.
928+
# Any unrecognized objects will be caught by _validate_input.
929+
for var in variables:
930+
if var in self._con_index_map:
931+
cons_to_validate.append(var)
932+
else:
933+
vars_to_validate.append(var)
934+
for con in constraints:
935+
if con in self._var_index_map:
936+
vars_to_validate.append(con)
937+
else:
938+
cons_to_validate.append(con)
939+
940+
variables, constraints = self._validate_input(
941+
vars_to_validate, cons_to_validate
942+
)
943+
v_exclude = ComponentSet(variables)
944+
c_exclude = ComponentSet(constraints)
945+
vars_to_include = [v for v in self.variables if v not in v_exclude]
946+
cons_to_include = [c for c in self.constraints if c not in c_exclude]
888947
incidence_graph = self._extract_subgraph(vars_to_include, cons_to_include)
889948
# update attributes
890949
self._variables = vars_to_include
891950
self._constraints = cons_to_include
892951
self._incidence_graph = incidence_graph
893952
self._var_index_map = ComponentMap(
894-
(var, i) for i, var in enumerate(self.variables)
953+
(var, i) for i, var in enumerate(vars_to_include)
895954
)
896955
self._con_index_map = ComponentMap(
897-
(con, i) for i, con in enumerate(self._constraints)
956+
(con, i) for i, con in enumerate(cons_to_include)
898957
)
899958

900959
def plot(self, variables=None, constraints=None, title=None, show=True):

pyomo/contrib/incidence_analysis/tests/test_interface.py

+43-11
Original file line numberDiff line numberDiff line change
@@ -634,17 +634,15 @@ def test_exception(self):
634634
nlp = PyomoNLP(model)
635635
igraph = IncidenceGraphInterface(nlp)
636636

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

643-
with self.assertRaises(RuntimeError) as exc:
642+
with self.assertRaisesRegex(KeyError, "does not exist"):
644643
variables = [model.P]
645644
constraints = [model.ideal_gas]
646645
igraph.block_triangularize(variables, constraints)
647-
self.assertIn("must be unindexed", str(exc.exception))
648646

649647

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

888-
with self.assertRaises(RuntimeError) as exc:
886+
with self.assertRaisesRegex(KeyError, "does not exist"):
889887
variables = [model.P]
890888
constraints = [model.ideal_gas]
891889
igraph.maximum_matching(variables, constraints)
892-
self.assertIn("must be unindexed", str(exc.exception))
893890

894-
with self.assertRaises(RuntimeError) as exc:
891+
with self.assertRaisesRegex(KeyError, "does not exist"):
895892
variables = [model.P]
896893
constraints = [model.ideal_gas]
897894
igraph.block_triangularize(variables, constraints)
898-
self.assertIn("must be unindexed", str(exc.exception))
899895

900896
@unittest.skipUnless(scipy_available, "scipy is not available.")
901897
def test_remove(self):
@@ -923,7 +919,7 @@ def test_remove(self):
923919
# Say we know that these variables and constraints should
924920
# be matched...
925921
vars_to_remove = [model.F[0], model.F[2]]
926-
cons_to_remove = (model.mbal[1], model.mbal[2])
922+
cons_to_remove = [model.mbal[1], model.mbal[2]]
927923
igraph.remove_nodes(vars_to_remove, cons_to_remove)
928924
variable_set = ComponentSet(igraph.variables)
929925
self.assertNotIn(model.F[0], variable_set)
@@ -1309,7 +1305,7 @@ def test_remove(self):
13091305
# matrix.
13101306
vars_to_remove = [m.flow_comp[1]]
13111307
cons_to_remove = [m.flow_eqn[1]]
1312-
igraph.remove_nodes(vars_to_remove + cons_to_remove)
1308+
igraph.remove_nodes(vars_to_remove, cons_to_remove)
13131309
var_dmp, con_dmp = igraph.dulmage_mendelsohn()
13141310
var_con_set = ComponentSet(igraph.variables + igraph.constraints)
13151311
underconstrained_set = ComponentSet(
@@ -1460,6 +1456,42 @@ def test_remove_no_matrix(self):
14601456
with self.assertRaisesRegex(RuntimeError, "no incidence matrix"):
14611457
igraph.remove_nodes([m.v1])
14621458

1459+
def test_remove_bad_node(self):
1460+
m = pyo.ConcreteModel()
1461+
m.x = pyo.Var([1, 2, 3])
1462+
m.eq = pyo.Constraint(pyo.PositiveIntegers)
1463+
m.eq[1] = m.x[1] * m.x[2] == m.x[3]
1464+
m.eq[2] = m.x[1] + 2 * m.x[2] == 3 * m.x[3]
1465+
igraph = IncidenceGraphInterface(m)
1466+
with self.assertRaisesRegex(KeyError, "does not exist"):
1467+
# Suppose we think something like this should work. We should get
1468+
# an error, and not silently do nothing.
1469+
igraph.remove_nodes([m.x], [m.eq[1]])
1470+
1471+
with self.assertRaisesRegex(KeyError, "does not exist"):
1472+
igraph.remove_nodes(None, [m.eq])
1473+
1474+
with self.assertRaisesRegex(KeyError, "does not exist"):
1475+
igraph.remove_nodes([[m.x[1], m.x[2]], [m.eq[1]]])
1476+
1477+
def test_remove_varcon_samelist_deprecated(self):
1478+
m = pyo.ConcreteModel()
1479+
m.x = pyo.Var([1, 2, 3])
1480+
m.eq = pyo.Constraint(pyo.PositiveIntegers)
1481+
m.eq[1] = m.x[1] * m.x[2] == m.x[3]
1482+
m.eq[2] = m.x[1] + 2 * m.x[2] == 3 * m.x[3]
1483+
1484+
igraph = IncidenceGraphInterface(m)
1485+
# This raises a deprecation warning. When the deprecated functionality
1486+
# is removed, this will fail, and this test should be updated accordingly.
1487+
igraph.remove_nodes([m.eq[1], m.x[1]])
1488+
self.assertEqual(len(igraph.variables), 2)
1489+
self.assertEqual(len(igraph.constraints), 1)
1490+
1491+
igraph.remove_nodes([], [m.eq[2], m.x[2]])
1492+
self.assertEqual(len(igraph.variables), 1)
1493+
self.assertEqual(len(igraph.constraints), 0)
1494+
14631495

14641496
@unittest.skipUnless(networkx_available, "networkx is not available.")
14651497
@unittest.skipUnless(scipy_available, "scipy is not available.")
@@ -1840,7 +1872,7 @@ def test_var_elim(self):
18401872
for adj_con in igraph.get_adjacent_to(m.x[1]):
18411873
for adj_var in igraph.get_adjacent_to(m.eq4):
18421874
igraph.add_edge(adj_var, adj_con)
1843-
igraph.remove_nodes([m.x[1], m.eq4])
1875+
igraph.remove_nodes([m.x[1]], [m.eq4])
18441876

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

pyomo/contrib/pynumero/algorithms/solvers/cyipopt_solver.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,13 @@ def license_is_valid(self):
319319
return True
320320

321321
def version(self):
322-
return tuple(int(_) for _ in cyipopt.__version__.split("."))
322+
def _int(x):
323+
try:
324+
return int(x)
325+
except:
326+
return x
327+
328+
return tuple(_int(_) for _ in cyipopt_interface.cyipopt.__version__.split("."))
323329

324330
def solve(self, model, **kwds):
325331
config = self.config(kwds, preserve_implicit=True)

0 commit comments

Comments
 (0)