Skip to content

Commit c849905

Browse files
authored
Merge pull request #3202 from jsiirola/legacy-solver-updates
Update `LegacySolverWrapper` to be compatible with the `pyomo` script
2 parents 4103225 + 3e7e7a2 commit c849905

File tree

6 files changed

+89
-53
lines changed

6 files changed

+89
-53
lines changed

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

+75-43
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,10 @@
1919
from pyomo.core.base.param import _ParamData
2020
from pyomo.core.base.block import _BlockData
2121
from pyomo.core.base.objective import _GeneralObjectiveData
22-
from pyomo.common.config import document_kwargs_from_configdict
22+
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
25+
from pyomo.common.modeling import NOTSET
2526
from pyomo.opt.results.results_ import SolverResults as LegacySolverResults
2627
from pyomo.opt.results.solution import Solution as LegacySolution
2728
from pyomo.core.kernel.objective import minimize
@@ -347,6 +348,11 @@ class LegacySolverWrapper:
347348
interface. Necessary for backwards compatibility.
348349
"""
349350

351+
def __init__(self, solver_io=None, **kwargs):
352+
if solver_io is not None:
353+
raise NotImplementedError('Still working on this')
354+
super().__init__(**kwargs)
355+
350356
#
351357
# Support "with" statements
352358
#
@@ -358,51 +364,61 @@ def __exit__(self, t, v, traceback):
358364

359365
def _map_config(
360366
self,
361-
tee,
362-
load_solutions,
363-
symbolic_solver_labels,
364-
timelimit,
365-
# Report timing is no longer a valid option. We now always return a
366-
# timer object that can be inspected.
367-
report_timing,
368-
raise_exception_on_nonoptimal_result,
369-
solver_io,
370-
suffixes,
371-
logfile,
372-
keepfiles,
373-
solnfile,
374-
options,
367+
tee=NOTSET,
368+
load_solutions=NOTSET,
369+
symbolic_solver_labels=NOTSET,
370+
timelimit=NOTSET,
371+
report_timing=NOTSET,
372+
raise_exception_on_nonoptimal_result=NOTSET,
373+
solver_io=NOTSET,
374+
suffixes=NOTSET,
375+
logfile=NOTSET,
376+
keepfiles=NOTSET,
377+
solnfile=NOTSET,
378+
options=NOTSET,
375379
):
376380
"""Map between legacy and new interface configuration options"""
377381
self.config = self.config()
378-
self.config.tee = tee
379-
self.config.load_solutions = load_solutions
380-
self.config.symbolic_solver_labels = symbolic_solver_labels
381-
self.config.time_limit = timelimit
382-
self.config.solver_options.set_value(options)
382+
if 'report_timing' not in self.config:
383+
self.config.declare(
384+
'report_timing', ConfigValue(domain=bool, default=False)
385+
)
386+
if tee is not NOTSET:
387+
self.config.tee = tee
388+
if load_solutions is not NOTSET:
389+
self.config.load_solutions = load_solutions
390+
if symbolic_solver_labels is not NOTSET:
391+
self.config.symbolic_solver_labels = symbolic_solver_labels
392+
if timelimit is not NOTSET:
393+
self.config.time_limit = timelimit
394+
if report_timing is not NOTSET:
395+
self.config.report_timing = report_timing
396+
if options is not NOTSET:
397+
self.config.solver_options.set_value(options)
383398
# This is a new flag in the interface. To preserve backwards compatibility,
384399
# its default is set to "False"
385-
self.config.raise_exception_on_nonoptimal_result = (
386-
raise_exception_on_nonoptimal_result
387-
)
388-
if solver_io is not None:
400+
if raise_exception_on_nonoptimal_result is not NOTSET:
401+
self.config.raise_exception_on_nonoptimal_result = (
402+
raise_exception_on_nonoptimal_result
403+
)
404+
if solver_io is not NOTSET and solver_io is not None:
389405
raise NotImplementedError('Still working on this')
390-
if suffixes is not None:
406+
if suffixes is not NOTSET and suffixes is not None:
391407
raise NotImplementedError('Still working on this')
392-
if logfile is not None:
408+
if logfile is not NOTSET and logfile is not None:
393409
raise NotImplementedError('Still working on this')
394410
if keepfiles or 'keepfiles' in self.config:
395411
cwd = os.getcwd()
396412
deprecation_warning(
397413
"`keepfiles` has been deprecated in the new solver interface. "
398-
"Use `working_dir` instead to designate a directory in which "
399-
f"files should be generated and saved. Setting `working_dir` to `{cwd}`.",
414+
"Use `working_dir` instead to designate a directory in which files "
415+
f"should be generated and saved. Setting `working_dir` to `{cwd}`.",
400416
version='6.7.1',
401417
)
402418
self.config.working_dir = cwd
403419
# I believe this currently does nothing; however, it is unclear what
404420
# our desired behavior is for this.
405-
if solnfile is not None:
421+
if solnfile is not NOTSET:
406422
if 'filename' in self.config:
407423
filename = os.path.splitext(solnfile)[0]
408424
self.config.filename = filename
@@ -504,28 +520,34 @@ def solve(
504520
505521
"""
506522
original_config = self.config
507-
self._map_config(
508-
tee,
509-
load_solutions,
510-
symbolic_solver_labels,
511-
timelimit,
512-
report_timing,
513-
raise_exception_on_nonoptimal_result,
514-
solver_io,
515-
suffixes,
516-
logfile,
517-
keepfiles,
518-
solnfile,
519-
options,
523+
524+
map_args = (
525+
'tee',
526+
'load_solutions',
527+
'symbolic_solver_labels',
528+
'timelimit',
529+
'report_timing',
530+
'raise_exception_on_nonoptimal_result',
531+
'solver_io',
532+
'suffixes',
533+
'logfile',
534+
'keepfiles',
535+
'solnfile',
536+
'options',
520537
)
538+
loc = locals()
539+
filtered_args = {k: loc[k] for k in map_args if loc.get(k, None) is not None}
540+
self._map_config(**filtered_args)
521541

522542
results: Results = super().solve(model)
523543
legacy_results, legacy_soln = self._map_results(model, results)
524-
525544
legacy_results = self._solution_handler(
526545
load_solutions, model, results, legacy_results, legacy_soln
527546
)
528547

548+
if self.config.report_timing:
549+
print(results.timing_info.timer)
550+
529551
self.config = original_config
530552

531553
return legacy_results
@@ -555,3 +577,13 @@ def license_is_valid(self) -> bool:
555577
556578
"""
557579
return bool(self.available())
580+
581+
def config_block(self, init=False):
582+
from pyomo.scripting.solve_config import default_config_block
583+
584+
return default_config_block(self, init)[0]
585+
586+
def set_options(self, options):
587+
opts = {k: v for k, v in options.value().items() if v is not None}
588+
if opts:
589+
self._map_config(**opts)

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

+3-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ def decorator(cls):
2727
class LegacySolver(LegacySolverWrapper, cls):
2828
pass
2929

30-
LegacySolverFactory.register(legacy_name, doc)(LegacySolver)
30+
LegacySolverFactory.register(legacy_name, doc + " (new interface)")(
31+
LegacySolver
32+
)
3133

3234
return cls
3335

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

-2
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030
from pyomo.repn.plugins.nl_writer import NLWriter, NLWriterInfo
3131
from pyomo.contrib.solver.base import SolverBase
3232
from pyomo.contrib.solver.config import SolverConfig
33-
from pyomo.contrib.solver.factory import SolverFactory
3433
from pyomo.contrib.solver.results import Results, TerminationCondition, SolutionStatus
3534
from pyomo.contrib.solver.sol_reader import parse_sol_file
3635
from pyomo.contrib.solver.solution import SolSolutionLoader
@@ -197,7 +196,6 @@ def get_reduced_costs(
197196
}
198197

199198

200-
@SolverFactory.register('ipopt_v2', doc='The ipopt NLP solver (new interface)')
201199
class Ipopt(SolverBase):
202200
CONFIG = IpoptConfig()
203201

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@
1717

1818
def load():
1919
SolverFactory.register(
20-
name='ipopt', legacy_name='ipopt_v2', doc='The IPOPT NLP solver (new interface)'
20+
name='ipopt', legacy_name='ipopt_v2', doc='The IPOPT NLP solver'
2121
)(Ipopt)
2222
SolverFactory.register(
23-
name='gurobi', legacy_name='gurobi_v2', doc='New interface to Gurobi'
23+
name='gurobi', legacy_name='gurobi_v2', doc='Persistent interface to Gurobi'
2424
)(Gurobi)

Diff for: pyomo/contrib/solver/tests/solvers/test_ipopt.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ def test_ipopt_config(self):
4848
self.assertIsInstance(config.executable, ExecutableData)
4949

5050
# Test custom initialization
51-
solver = SolverFactory('ipopt_v2', executable='/path/to/exe')
51+
solver = SolverFactory('ipopt', executable='/path/to/exe')
5252
self.assertFalse(solver.config.tee)
5353
self.assertTrue(solver.config.executable.startswith('/path'))
5454

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

+8-4
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,13 @@ def test_context_manager(self):
178178

179179
class TestLegacySolverWrapper(unittest.TestCase):
180180
def test_class_method_list(self):
181-
expected_list = ['available', 'license_is_valid', 'solve']
181+
expected_list = [
182+
'available',
183+
'config_block',
184+
'license_is_valid',
185+
'set_options',
186+
'solve',
187+
]
182188
method_list = [
183189
method
184190
for method in dir(base.LegacySolverWrapper)
@@ -207,9 +213,7 @@ def test_map_config(self):
207213
self.assertTrue(instance.config.tee)
208214
self.assertFalse(instance.config.load_solutions)
209215
self.assertEqual(instance.config.time_limit, 20)
210-
# Report timing shouldn't be created because it no longer exists
211-
with self.assertRaises(AttributeError):
212-
print(instance.config.report_timing)
216+
self.assertEqual(instance.config.report_timing, True)
213217
# Keepfiles should not be created because we did not declare keepfiles on
214218
# the original config
215219
with self.assertRaises(AttributeError):

0 commit comments

Comments
 (0)