Skip to content

Commit c68ce66

Browse files
authored
Merge pull request #3214 from mrmundt/sol-refac-nconstraint
BUG FIXES: Solver Refactor for IDAES Integration
2 parents f15a1da + 03830c0 commit c68ce66

File tree

3 files changed

+177
-10
lines changed

3 files changed

+177
-10
lines changed

Diff for: 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

Diff for: pyomo/contrib/solver/base.py

+53-10
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@
1414
from typing import Sequence, Dict, Optional, Mapping, NoReturn, List, Tuple
1515
import os
1616

17-
from pyomo.core.base.constraint import _GeneralConstraintData
18-
from pyomo.core.base.var import _GeneralVarData
17+
from pyomo.core.base.constraint import Constraint, _GeneralConstraintData
18+
from pyomo.core.base.var import Var, _GeneralVarData
1919
from pyomo.core.base.param import _ParamData
2020
from pyomo.core.base.block import _BlockData
21-
from pyomo.core.base.objective import _GeneralObjectiveData
21+
from pyomo.core.base.objective import Objective, _GeneralObjectiveData
2222
from pyomo.common.config import document_kwargs_from_configdict, ConfigValue
2323
from pyomo.common.errors import ApplicationError
2424
from pyomo.common.deprecation import deprecation_warning
@@ -348,9 +348,19 @@ class LegacySolverWrapper:
348348
interface. Necessary for backwards compatibility.
349349
"""
350350

351-
def __init__(self, solver_io=None, **kwargs):
352-
if solver_io is not None:
351+
def __init__(self, **kwargs):
352+
if 'solver_io' in kwargs:
353353
raise NotImplementedError('Still working on this')
354+
# There is no reason for a user to be trying to mix both old
355+
# and new options. That is silly. So we will yell at them.
356+
self.options = kwargs.pop('options', None)
357+
if 'solver_options' in kwargs:
358+
if self.options is not None:
359+
raise ValueError(
360+
"Both 'options' and 'solver_options' were requested. "
361+
"Please use one or the other, not both."
362+
)
363+
self.options = kwargs.pop('solver_options')
354364
super().__init__(**kwargs)
355365

356366
#
@@ -376,6 +386,8 @@ def _map_config(
376386
keepfiles=NOTSET,
377387
solnfile=NOTSET,
378388
options=NOTSET,
389+
solver_options=NOTSET,
390+
writer_config=NOTSET,
379391
):
380392
"""Map between legacy and new interface configuration options"""
381393
self.config = self.config()
@@ -393,8 +405,26 @@ def _map_config(
393405
self.config.time_limit = timelimit
394406
if report_timing is not NOTSET:
395407
self.config.report_timing = report_timing
396-
if options is not NOTSET:
408+
if self.options is not None:
409+
self.config.solver_options.set_value(self.options)
410+
if (options is not NOTSET) and (solver_options is not NOTSET):
411+
# There is no reason for a user to be trying to mix both old
412+
# and new options. That is silly. So we will yell at them.
413+
# Example that would raise an error:
414+
# solver.solve(model, options={'foo' : 'bar'}, solver_options={'foo' : 'not_bar'})
415+
raise ValueError(
416+
"Both 'options' and 'solver_options' were requested. "
417+
"Please use one or the other, not both."
418+
)
419+
elif options is not NOTSET:
420+
# This block is trying to mimic the existing logic in the legacy
421+
# interface that allows users to pass initialized options to
422+
# the solver object and override them in the solve call.
397423
self.config.solver_options.set_value(options)
424+
elif solver_options is not NOTSET:
425+
self.config.solver_options.set_value(solver_options)
426+
if writer_config is not NOTSET:
427+
self.config.writer_config.set_value(writer_config)
398428
# This is a new flag in the interface. To preserve backwards compatibility,
399429
# its default is set to "False"
400430
if raise_exception_on_nonoptimal_result is not NOTSET:
@@ -435,9 +465,14 @@ def _map_results(self, model, results):
435465
]
436466
legacy_soln.status = legacy_solution_status_map[results.solution_status]
437467
legacy_results.solver.termination_message = str(results.termination_condition)
438-
legacy_results.problem.number_of_constraints = model.nconstraints()
439-
legacy_results.problem.number_of_variables = model.nvariables()
440-
number_of_objectives = model.nobjectives()
468+
legacy_results.problem.number_of_constraints = float('nan')
469+
legacy_results.problem.number_of_variables = float('nan')
470+
number_of_objectives = sum(
471+
1
472+
for _ in model.component_data_objects(
473+
Objective, active=True, descend_into=True
474+
)
475+
)
441476
legacy_results.problem.number_of_objectives = number_of_objectives
442477
if number_of_objectives == 1:
443478
obj = get_objective(model)
@@ -508,7 +543,10 @@ def solve(
508543
options: Optional[Dict] = None,
509544
keepfiles: bool = False,
510545
symbolic_solver_labels: bool = False,
546+
# These are for forward-compatibility
511547
raise_exception_on_nonoptimal_result: bool = False,
548+
solver_options: Optional[Dict] = None,
549+
writer_config: Optional[Dict] = None,
512550
):
513551
"""
514552
Solve method: maps new solve method style to backwards compatible version.
@@ -534,6 +572,8 @@ def solve(
534572
'keepfiles',
535573
'solnfile',
536574
'options',
575+
'solver_options',
576+
'writer_config',
537577
)
538578
loc = locals()
539579
filtered_args = {k: loc[k] for k in map_args if loc.get(k, None) is not None}
@@ -559,7 +599,10 @@ def available(self, exception_flag=True):
559599
"""
560600
ans = super().available()
561601
if exception_flag and not ans:
562-
raise ApplicationError(f'Solver {self.__class__} is not available ({ans}).')
602+
raise ApplicationError(
603+
f'Solver "{self.name}" is not available. '
604+
f'The returned status is: {ans}.'
605+
)
563606
return bool(ans)
564607

565608
def license_is_valid(self) -> bool:

Diff for: pyomo/contrib/solver/tests/unit/test_base.py

+93
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,99 @@ def test_map_config(self):
272272
with self.assertRaises(AttributeError):
273273
print(instance.config.keepfiles)
274274

275+
def test_solver_options_behavior(self):
276+
# options can work in multiple ways (set from instantiation, set
277+
# after instantiation, set during solve).
278+
# Test case 1: Set at instantiation
279+
solver = base.LegacySolverWrapper(options={'max_iter': 6})
280+
self.assertEqual(solver.options, {'max_iter': 6})
281+
282+
# Test case 2: Set later
283+
solver = base.LegacySolverWrapper()
284+
solver.options = {'max_iter': 4, 'foo': 'bar'}
285+
self.assertEqual(solver.options, {'max_iter': 4, 'foo': 'bar'})
286+
287+
# Test case 3: pass some options to the mapping (aka, 'solve' command)
288+
solver = base.LegacySolverWrapper()
289+
config = ConfigDict(implicit=True)
290+
config.declare(
291+
'solver_options',
292+
ConfigDict(implicit=True, description="Options to pass to the solver."),
293+
)
294+
solver.config = config
295+
solver._map_config(options={'max_iter': 4})
296+
self.assertEqual(solver.config.solver_options, {'max_iter': 4})
297+
298+
# Test case 4: Set at instantiation and override during 'solve' call
299+
solver = base.LegacySolverWrapper(options={'max_iter': 6})
300+
config = ConfigDict(implicit=True)
301+
config.declare(
302+
'solver_options',
303+
ConfigDict(implicit=True, description="Options to pass to the solver."),
304+
)
305+
solver.config = config
306+
solver._map_config(options={'max_iter': 4})
307+
self.assertEqual(solver.config.solver_options, {'max_iter': 4})
308+
self.assertEqual(solver.options, {'max_iter': 6})
309+
310+
# solver_options are also supported
311+
# Test case 1: set at instantiation
312+
solver = base.LegacySolverWrapper(solver_options={'max_iter': 6})
313+
self.assertEqual(solver.options, {'max_iter': 6})
314+
315+
# Test case 2: pass some solver_options to the mapping (aka, 'solve' command)
316+
solver = base.LegacySolverWrapper()
317+
config = ConfigDict(implicit=True)
318+
config.declare(
319+
'solver_options',
320+
ConfigDict(implicit=True, description="Options to pass to the solver."),
321+
)
322+
solver.config = config
323+
solver._map_config(solver_options={'max_iter': 4})
324+
self.assertEqual(solver.config.solver_options, {'max_iter': 4})
325+
326+
# Test case 3: Set at instantiation and override during 'solve' call
327+
solver = base.LegacySolverWrapper(solver_options={'max_iter': 6})
328+
config = ConfigDict(implicit=True)
329+
config.declare(
330+
'solver_options',
331+
ConfigDict(implicit=True, description="Options to pass to the solver."),
332+
)
333+
solver.config = config
334+
solver._map_config(solver_options={'max_iter': 4})
335+
self.assertEqual(solver.config.solver_options, {'max_iter': 4})
336+
self.assertEqual(solver.options, {'max_iter': 6})
337+
338+
# users can mix... sort of
339+
# Test case 1: Initialize with options, solve with solver_options
340+
solver = base.LegacySolverWrapper(options={'max_iter': 6})
341+
config = ConfigDict(implicit=True)
342+
config.declare(
343+
'solver_options',
344+
ConfigDict(implicit=True, description="Options to pass to the solver."),
345+
)
346+
solver.config = config
347+
solver._map_config(solver_options={'max_iter': 4})
348+
self.assertEqual(solver.config.solver_options, {'max_iter': 4})
349+
350+
# users CANNOT initialize both values at the same time, because how
351+
# do we know what to do with it then?
352+
# Test case 1: Class instance
353+
with self.assertRaises(ValueError):
354+
solver = base.LegacySolverWrapper(
355+
options={'max_iter': 6}, solver_options={'max_iter': 4}
356+
)
357+
# Test case 2: Passing to `solve`
358+
solver = base.LegacySolverWrapper()
359+
config = ConfigDict(implicit=True)
360+
config.declare(
361+
'solver_options',
362+
ConfigDict(implicit=True, description="Options to pass to the solver."),
363+
)
364+
solver.config = config
365+
with self.assertRaises(ValueError):
366+
solver._map_config(solver_options={'max_iter': 4}, options={'max_iter': 6})
367+
275368
def test_map_results(self):
276369
# Unclear how to test this
277370
pass

0 commit comments

Comments
 (0)