Skip to content

Commit 476766e

Browse files
authored
Merge pull request #3161 from michaelbynum/zero_presolve
fix division by zero error in linear presolve
2 parents a4dc0c2 + 9bf36c7 commit 476766e

File tree

4 files changed

+233
-21
lines changed

4 files changed

+233
-21
lines changed

pyomo/contrib/solver/ipopt.py

+27-17
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,11 @@
1717

1818
from pyomo.common import Executable
1919
from pyomo.common.config import ConfigValue, document_kwargs_from_configdict, ConfigDict
20-
from pyomo.common.errors import PyomoException, DeveloperError
20+
from pyomo.common.errors import (
21+
PyomoException,
22+
DeveloperError,
23+
InfeasibleConstraintException,
24+
)
2125
from pyomo.common.tempfiles import TempfileManager
2226
from pyomo.common.timing import HierarchicalTimer
2327
from pyomo.core.base.var import _GeneralVarData
@@ -72,11 +76,7 @@ def __init__(
7276
),
7377
)
7478
self.writer_config: ConfigDict = self.declare(
75-
'writer_config',
76-
ConfigValue(
77-
default=NLWriter.CONFIG(),
78-
description="Configuration that controls options in the NL writer.",
79-
),
79+
'writer_config', NLWriter.CONFIG()
8080
)
8181

8282

@@ -314,15 +314,19 @@ def solve(self, model, **kwds):
314314
) as row_file, open(basename + '.col', 'w') as col_file:
315315
timer.start('write_nl_file')
316316
self._writer.config.set_value(config.writer_config)
317-
nl_info = self._writer.write(
318-
model,
319-
nl_file,
320-
row_file,
321-
col_file,
322-
symbolic_solver_labels=config.symbolic_solver_labels,
323-
)
317+
try:
318+
nl_info = self._writer.write(
319+
model,
320+
nl_file,
321+
row_file,
322+
col_file,
323+
symbolic_solver_labels=config.symbolic_solver_labels,
324+
)
325+
proven_infeasible = False
326+
except InfeasibleConstraintException:
327+
proven_infeasible = True
324328
timer.stop('write_nl_file')
325-
if len(nl_info.variables) > 0:
329+
if not proven_infeasible and len(nl_info.variables) > 0:
326330
# Get a copy of the environment to pass to the subprocess
327331
env = os.environ.copy()
328332
if nl_info.external_function_libraries:
@@ -361,11 +365,17 @@ def solve(self, model, **kwds):
361365
timer.stop('subprocess')
362366
# This is the stuff we need to parse to get the iterations
363367
# and time
364-
iters, ipopt_time_nofunc, ipopt_time_func, ipopt_total_time = (
368+
(iters, ipopt_time_nofunc, ipopt_time_func, ipopt_total_time) = (
365369
self._parse_ipopt_output(ostreams[0])
366370
)
367371

368-
if len(nl_info.variables) == 0:
372+
if proven_infeasible:
373+
results = Results()
374+
results.termination_condition = TerminationCondition.provenInfeasible
375+
results.solution_loader = SolSolutionLoader(None, None)
376+
results.iteration_count = 0
377+
results.timing_info.total_seconds = 0
378+
elif len(nl_info.variables) == 0:
369379
if len(nl_info.eliminated_vars) == 0:
370380
results = Results()
371381
results.termination_condition = TerminationCondition.emptyModel
@@ -457,7 +467,7 @@ def solve(self, model, **kwds):
457467
)
458468

459469
results.solver_configuration = config
460-
if len(nl_info.variables) > 0:
470+
if not proven_infeasible and len(nl_info.variables) > 0:
461471
results.solver_log = ostreams[0].getvalue()
462472

463473
# Capture/record end-time / wall-time

pyomo/contrib/solver/tests/solvers/test_solvers.py

+65
Original file line numberDiff line numberDiff line change
@@ -1508,6 +1508,71 @@ def test_bug_2(self, name: str, opt_class: Type[SolverBase], use_presolve: bool)
15081508
res = opt.solve(m)
15091509
self.assertAlmostEqual(res.incumbent_objective, -18, 5)
15101510

1511+
@parameterized.expand(input=_load_tests(nl_solvers))
1512+
def test_presolve_with_zero_coef(
1513+
self, name: str, opt_class: Type[SolverBase], use_presolve: bool
1514+
):
1515+
opt: SolverBase = opt_class()
1516+
if not opt.available():
1517+
raise unittest.SkipTest(f'Solver {opt.name} not available.')
1518+
if use_presolve:
1519+
opt.config.writer_config.linear_presolve = True
1520+
else:
1521+
opt.config.writer_config.linear_presolve = False
1522+
1523+
"""
1524+
when c2 gets presolved out, c1 becomes
1525+
x - y + y = 0 which becomes
1526+
x - 0*y == 0 which is the zero we are testing for
1527+
"""
1528+
m = pe.ConcreteModel()
1529+
m.x = pe.Var()
1530+
m.y = pe.Var()
1531+
m.z = pe.Var()
1532+
m.obj = pe.Objective(expr=m.x**2 + m.y**2 + m.z**2)
1533+
m.c1 = pe.Constraint(expr=m.x == m.y + m.z + 1.5)
1534+
m.c2 = pe.Constraint(expr=m.z == -m.y)
1535+
1536+
res = opt.solve(m)
1537+
self.assertAlmostEqual(res.incumbent_objective, 2.25)
1538+
self.assertAlmostEqual(m.x.value, 1.5)
1539+
self.assertAlmostEqual(m.y.value, 0)
1540+
self.assertAlmostEqual(m.z.value, 0)
1541+
1542+
m.x.setlb(2)
1543+
res = opt.solve(
1544+
m, load_solutions=False, raise_exception_on_nonoptimal_result=False
1545+
)
1546+
if use_presolve:
1547+
exp = TerminationCondition.provenInfeasible
1548+
else:
1549+
exp = TerminationCondition.locallyInfeasible
1550+
self.assertEqual(res.termination_condition, exp)
1551+
1552+
m = pe.ConcreteModel()
1553+
m.w = pe.Var()
1554+
m.x = pe.Var()
1555+
m.y = pe.Var()
1556+
m.z = pe.Var()
1557+
m.obj = pe.Objective(expr=m.x**2 + m.y**2 + m.z**2 + m.w**2)
1558+
m.c1 = pe.Constraint(expr=m.x + m.w == m.y + m.z)
1559+
m.c2 = pe.Constraint(expr=m.z == -m.y)
1560+
m.c3 = pe.Constraint(expr=m.x == -m.w)
1561+
1562+
res = opt.solve(m)
1563+
self.assertAlmostEqual(res.incumbent_objective, 0)
1564+
self.assertAlmostEqual(m.w.value, 0)
1565+
self.assertAlmostEqual(m.x.value, 0)
1566+
self.assertAlmostEqual(m.y.value, 0)
1567+
self.assertAlmostEqual(m.z.value, 0)
1568+
1569+
del m.c1
1570+
m.c1 = pe.Constraint(expr=m.x + m.w == m.y + m.z + 1.5)
1571+
res = opt.solve(
1572+
m, load_solutions=False, raise_exception_on_nonoptimal_result=False
1573+
)
1574+
self.assertEqual(res.termination_condition, exp)
1575+
15111576
@parameterized.expand(input=_load_tests(all_solvers))
15121577
def test_scaling(self, name: str, opt_class: Type[SolverBase], use_presolve: bool):
15131578
opt: SolverBase = opt_class()

pyomo/repn/plugins/nl_writer.py

+16-4
Original file line numberDiff line numberDiff line change
@@ -1760,7 +1760,7 @@ def _linear_presolve(self, comp_by_linear_var, lcon_by_linear_nnz, var_bounds):
17601760
id2_isdiscrete = var_map[id2].domain.isdiscrete()
17611761
if var_map[_id].domain.isdiscrete() ^ id2_isdiscrete:
17621762
# if only one variable is discrete, then we need to
1763-
# substiitute out the other
1763+
# substitute out the other
17641764
if id2_isdiscrete:
17651765
_id, id2 = id2, _id
17661766
coef, coef2 = coef2, coef
@@ -1820,21 +1820,33 @@ def _linear_presolve(self, comp_by_linear_var, lcon_by_linear_nnz, var_bounds):
18201820
# appropriately (that expr_info is persisting in the
18211821
# eliminated_vars dict - and we will use that to
18221822
# update other linear expressions later.)
1823+
old_nnz = len(expr_info.linear)
18231824
c = expr_info.linear.pop(_id, 0)
1825+
nnz = old_nnz - 1
18241826
expr_info.const += c * b
18251827
if x in expr_info.linear:
18261828
expr_info.linear[x] += c * a
1829+
if expr_info.linear[x] == 0:
1830+
nnz -= 1
1831+
coef = expr_info.linear.pop(x)
18271832
elif a:
18281833
expr_info.linear[x] = c * a
18291834
# replacing _id with x... NNZ is not changing,
18301835
# but we need to remember that x is now part of
18311836
# this constraint
18321837
comp_by_linear_var[x].append((con_id, expr_info))
18331838
continue
1834-
# NNZ has been reduced by 1
1835-
nnz = len(expr_info.linear)
1836-
_old = lcon_by_linear_nnz[nnz + 1]
1839+
_old = lcon_by_linear_nnz[old_nnz]
18371840
if con_id in _old:
1841+
if not nnz:
1842+
if abs(expr_info.const) > TOL:
1843+
# constraint is trivially infeasible
1844+
raise InfeasibleConstraintException(
1845+
"model contains a trivially infeasible constraint "
1846+
f"{expr_info.const} == {coef}*{var_map[x]}"
1847+
)
1848+
# constraint is trivially feasible
1849+
eliminated_cons.add(con_id)
18381850
lcon_by_linear_nnz[nnz][con_id] = _old.pop(con_id)
18391851
# If variables were replaced by the variable that
18401852
# we are currently eliminating, then we need to update

pyomo/repn/tests/ampl/test_nlv2.py

+125
Original file line numberDiff line numberDiff line change
@@ -1683,6 +1683,131 @@ def test_presolve_named_expressions(self):
16831683
G0 2 #obj
16841684
0 0
16851685
1 0
1686+
""",
1687+
OUT.getvalue(),
1688+
)
1689+
)
1690+
1691+
def test_presolve_zero_coef(self):
1692+
m = ConcreteModel()
1693+
m.x = Var()
1694+
m.y = Var()
1695+
m.z = Var()
1696+
m.obj = Objective(expr=m.x**2 + m.y**2 + m.z**2)
1697+
m.c1 = Constraint(expr=m.x == m.y + m.z + 1.5)
1698+
m.c2 = Constraint(expr=m.z == -m.y)
1699+
1700+
OUT = io.StringIO()
1701+
with LoggingIntercept() as LOG:
1702+
nlinfo = nl_writer.NLWriter().write(
1703+
m, OUT, symbolic_solver_labels=True, linear_presolve=True
1704+
)
1705+
self.assertEqual(LOG.getvalue(), "")
1706+
1707+
self.assertEqual(nlinfo.eliminated_vars[0], (m.x, 1.5))
1708+
self.assertIs(nlinfo.eliminated_vars[1][0], m.y)
1709+
self.assertExpressionsEqual(
1710+
nlinfo.eliminated_vars[1][1], LinearExpression([-1.0 * m.z])
1711+
)
1712+
1713+
self.assertEqual(
1714+
*nl_diff(
1715+
"""g3 1 1 0 # problem unknown
1716+
1 0 1 0 0 #vars, constraints, objectives, ranges, eqns
1717+
0 1 0 0 0 0 #nonlinear constrs, objs; ccons: lin, nonlin, nd, nzlb
1718+
0 0 #network constraints: nonlinear, linear
1719+
0 1 0 #nonlinear vars in constraints, objectives, both
1720+
0 0 0 1 #linear network variables; functions; arith, flags
1721+
0 0 0 0 0 #discrete variables: binary, integer, nonlinear (b,c,o)
1722+
0 1 #nonzeros in Jacobian, obj. gradient
1723+
3 1 #max name lengths: constraints, variables
1724+
0 0 0 0 0 #common exprs: b,c,o,c1,o1
1725+
O0 0 #obj
1726+
o54 #sumlist
1727+
3 #(n)
1728+
o5 #^
1729+
n1.5
1730+
n2
1731+
o5 #^
1732+
o16 #-
1733+
v0 #z
1734+
n2
1735+
o5 #^
1736+
v0 #z
1737+
n2
1738+
x0 #initial guess
1739+
r #0 ranges (rhs's)
1740+
b #1 bounds (on variables)
1741+
3 #z
1742+
k0 #intermediate Jacobian column lengths
1743+
G0 1 #obj
1744+
0 0
1745+
""",
1746+
OUT.getvalue(),
1747+
)
1748+
)
1749+
1750+
m.c3 = Constraint(expr=m.x == 2)
1751+
OUT = io.StringIO()
1752+
with LoggingIntercept() as LOG:
1753+
with self.assertRaisesRegex(
1754+
nl_writer.InfeasibleConstraintException,
1755+
r"model contains a trivially infeasible constraint 0.5 == 0.0\*y",
1756+
):
1757+
nlinfo = nl_writer.NLWriter().write(
1758+
m, OUT, symbolic_solver_labels=True, linear_presolve=True
1759+
)
1760+
self.assertEqual(LOG.getvalue(), "")
1761+
1762+
m.c1.set_value(m.x >= m.y + m.z + 1.5)
1763+
OUT = io.StringIO()
1764+
with LoggingIntercept() as LOG:
1765+
nlinfo = nl_writer.NLWriter().write(
1766+
m, OUT, symbolic_solver_labels=True, linear_presolve=True
1767+
)
1768+
self.assertEqual(LOG.getvalue(), "")
1769+
1770+
self.assertIs(nlinfo.eliminated_vars[0][0], m.y)
1771+
self.assertExpressionsEqual(
1772+
nlinfo.eliminated_vars[0][1], LinearExpression([-1.0 * m.z])
1773+
)
1774+
self.assertEqual(nlinfo.eliminated_vars[1], (m.x, 2))
1775+
1776+
self.assertEqual(
1777+
*nl_diff(
1778+
"""g3 1 1 0 # problem unknown
1779+
1 1 1 0 0 #vars, constraints, objectives, ranges, eqns
1780+
0 1 0 0 0 0 #nonlinear constrs, objs; ccons: lin, nonlin, nd, nzlb
1781+
0 0 #network constraints: nonlinear, linear
1782+
0 1 0 #nonlinear vars in constraints, objectives, both
1783+
0 0 0 1 #linear network variables; functions; arith, flags
1784+
0 0 0 0 0 #discrete variables: binary, integer, nonlinear (b,c,o)
1785+
0 1 #nonzeros in Jacobian, obj. gradient
1786+
3 1 #max name lengths: constraints, variables
1787+
0 0 0 0 0 #common exprs: b,c,o,c1,o1
1788+
C0 #c1
1789+
n0
1790+
O0 0 #obj
1791+
o54 #sumlist
1792+
3 #(n)
1793+
o5 #^
1794+
n2
1795+
n2
1796+
o5 #^
1797+
o16 #-
1798+
v0 #z
1799+
n2
1800+
o5 #^
1801+
v0 #z
1802+
n2
1803+
x0 #initial guess
1804+
r #1 ranges (rhs's)
1805+
1 0.5 #c1
1806+
b #1 bounds (on variables)
1807+
3 #z
1808+
k0 #intermediate Jacobian column lengths
1809+
G0 1 #obj
1810+
0 0
16861811
""",
16871812
OUT.getvalue(),
16881813
)

0 commit comments

Comments
 (0)