Skip to content

Commit

Permalink
Merge pull request Pyomo#3202 from jsiirola/legacy-solver-updates
Browse files Browse the repository at this point in the history
Update `LegacySolverWrapper` to be compatible with the `pyomo` script
  • Loading branch information
mrmundt authored Mar 19, 2024
2 parents 4103225 + 3e7e7a2 commit c849905
Show file tree
Hide file tree
Showing 6 changed files with 89 additions and 53 deletions.
118 changes: 75 additions & 43 deletions pyomo/contrib/solver/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@
from pyomo.core.base.param import _ParamData
from pyomo.core.base.block import _BlockData
from pyomo.core.base.objective import _GeneralObjectiveData
from pyomo.common.config import document_kwargs_from_configdict
from pyomo.common.config import document_kwargs_from_configdict, ConfigValue
from pyomo.common.errors import ApplicationError
from pyomo.common.deprecation import deprecation_warning
from pyomo.common.modeling import NOTSET
from pyomo.opt.results.results_ import SolverResults as LegacySolverResults
from pyomo.opt.results.solution import Solution as LegacySolution
from pyomo.core.kernel.objective import minimize
Expand Down Expand Up @@ -347,6 +348,11 @@ class LegacySolverWrapper:
interface. Necessary for backwards compatibility.
"""

def __init__(self, solver_io=None, **kwargs):
if solver_io is not None:
raise NotImplementedError('Still working on this')
super().__init__(**kwargs)

#
# Support "with" statements
#
Expand All @@ -358,51 +364,61 @@ def __exit__(self, t, v, traceback):

def _map_config(
self,
tee,
load_solutions,
symbolic_solver_labels,
timelimit,
# Report timing is no longer a valid option. We now always return a
# timer object that can be inspected.
report_timing,
raise_exception_on_nonoptimal_result,
solver_io,
suffixes,
logfile,
keepfiles,
solnfile,
options,
tee=NOTSET,
load_solutions=NOTSET,
symbolic_solver_labels=NOTSET,
timelimit=NOTSET,
report_timing=NOTSET,
raise_exception_on_nonoptimal_result=NOTSET,
solver_io=NOTSET,
suffixes=NOTSET,
logfile=NOTSET,
keepfiles=NOTSET,
solnfile=NOTSET,
options=NOTSET,
):
"""Map between legacy and new interface configuration options"""
self.config = self.config()
self.config.tee = tee
self.config.load_solutions = load_solutions
self.config.symbolic_solver_labels = symbolic_solver_labels
self.config.time_limit = timelimit
self.config.solver_options.set_value(options)
if 'report_timing' not in self.config:
self.config.declare(
'report_timing', ConfigValue(domain=bool, default=False)
)
if tee is not NOTSET:
self.config.tee = tee
if load_solutions is not NOTSET:
self.config.load_solutions = load_solutions
if symbolic_solver_labels is not NOTSET:
self.config.symbolic_solver_labels = symbolic_solver_labels
if timelimit is not NOTSET:
self.config.time_limit = timelimit
if report_timing is not NOTSET:
self.config.report_timing = report_timing
if options is not NOTSET:
self.config.solver_options.set_value(options)
# This is a new flag in the interface. To preserve backwards compatibility,
# its default is set to "False"
self.config.raise_exception_on_nonoptimal_result = (
raise_exception_on_nonoptimal_result
)
if solver_io is not None:
if raise_exception_on_nonoptimal_result is not NOTSET:
self.config.raise_exception_on_nonoptimal_result = (
raise_exception_on_nonoptimal_result
)
if solver_io is not NOTSET and solver_io is not None:
raise NotImplementedError('Still working on this')
if suffixes is not None:
if suffixes is not NOTSET and suffixes is not None:
raise NotImplementedError('Still working on this')
if logfile is not None:
if logfile is not NOTSET and logfile is not None:
raise NotImplementedError('Still working on this')
if keepfiles or 'keepfiles' in self.config:
cwd = os.getcwd()
deprecation_warning(
"`keepfiles` has been deprecated in the new solver interface. "
"Use `working_dir` instead to designate a directory in which "
f"files should be generated and saved. Setting `working_dir` to `{cwd}`.",
"Use `working_dir` instead to designate a directory in which files "
f"should be generated and saved. Setting `working_dir` to `{cwd}`.",
version='6.7.1',
)
self.config.working_dir = cwd
# I believe this currently does nothing; however, it is unclear what
# our desired behavior is for this.
if solnfile is not None:
if solnfile is not NOTSET:
if 'filename' in self.config:
filename = os.path.splitext(solnfile)[0]
self.config.filename = filename
Expand Down Expand Up @@ -504,28 +520,34 @@ def solve(
"""
original_config = self.config
self._map_config(
tee,
load_solutions,
symbolic_solver_labels,
timelimit,
report_timing,
raise_exception_on_nonoptimal_result,
solver_io,
suffixes,
logfile,
keepfiles,
solnfile,
options,

map_args = (
'tee',
'load_solutions',
'symbolic_solver_labels',
'timelimit',
'report_timing',
'raise_exception_on_nonoptimal_result',
'solver_io',
'suffixes',
'logfile',
'keepfiles',
'solnfile',
'options',
)
loc = locals()
filtered_args = {k: loc[k] for k in map_args if loc.get(k, None) is not None}
self._map_config(**filtered_args)

results: Results = super().solve(model)
legacy_results, legacy_soln = self._map_results(model, results)

legacy_results = self._solution_handler(
load_solutions, model, results, legacy_results, legacy_soln
)

if self.config.report_timing:
print(results.timing_info.timer)

self.config = original_config

return legacy_results
Expand Down Expand Up @@ -555,3 +577,13 @@ def license_is_valid(self) -> bool:
"""
return bool(self.available())

def config_block(self, init=False):
from pyomo.scripting.solve_config import default_config_block

return default_config_block(self, init)[0]

def set_options(self, options):
opts = {k: v for k, v in options.value().items() if v is not None}
if opts:
self._map_config(**opts)
4 changes: 3 additions & 1 deletion pyomo/contrib/solver/factory.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ def decorator(cls):
class LegacySolver(LegacySolverWrapper, cls):
pass

LegacySolverFactory.register(legacy_name, doc)(LegacySolver)
LegacySolverFactory.register(legacy_name, doc + " (new interface)")(
LegacySolver
)

return cls

Expand Down
2 changes: 0 additions & 2 deletions pyomo/contrib/solver/ipopt.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
from pyomo.repn.plugins.nl_writer import NLWriter, NLWriterInfo
from pyomo.contrib.solver.base import SolverBase
from pyomo.contrib.solver.config import SolverConfig
from pyomo.contrib.solver.factory import SolverFactory
from pyomo.contrib.solver.results import Results, TerminationCondition, SolutionStatus
from pyomo.contrib.solver.sol_reader import parse_sol_file
from pyomo.contrib.solver.solution import SolSolutionLoader
Expand Down Expand Up @@ -197,7 +196,6 @@ def get_reduced_costs(
}


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

Expand Down
4 changes: 2 additions & 2 deletions pyomo/contrib/solver/plugins.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@

def load():
SolverFactory.register(
name='ipopt', legacy_name='ipopt_v2', doc='The IPOPT NLP solver (new interface)'
name='ipopt', legacy_name='ipopt_v2', doc='The IPOPT NLP solver'
)(Ipopt)
SolverFactory.register(
name='gurobi', legacy_name='gurobi_v2', doc='New interface to Gurobi'
name='gurobi', legacy_name='gurobi_v2', doc='Persistent interface to Gurobi'
)(Gurobi)
2 changes: 1 addition & 1 deletion pyomo/contrib/solver/tests/solvers/test_ipopt.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def test_ipopt_config(self):
self.assertIsInstance(config.executable, ExecutableData)

# Test custom initialization
solver = SolverFactory('ipopt_v2', executable='/path/to/exe')
solver = SolverFactory('ipopt', executable='/path/to/exe')
self.assertFalse(solver.config.tee)
self.assertTrue(solver.config.executable.startswith('/path'))

Expand Down
12 changes: 8 additions & 4 deletions pyomo/contrib/solver/tests/unit/test_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,13 @@ def test_context_manager(self):

class TestLegacySolverWrapper(unittest.TestCase):
def test_class_method_list(self):
expected_list = ['available', 'license_is_valid', 'solve']
expected_list = [
'available',
'config_block',
'license_is_valid',
'set_options',
'solve',
]
method_list = [
method
for method in dir(base.LegacySolverWrapper)
Expand Down Expand Up @@ -207,9 +213,7 @@ def test_map_config(self):
self.assertTrue(instance.config.tee)
self.assertFalse(instance.config.load_solutions)
self.assertEqual(instance.config.time_limit, 20)
# Report timing shouldn't be created because it no longer exists
with self.assertRaises(AttributeError):
print(instance.config.report_timing)
self.assertEqual(instance.config.report_timing, True)
# Keepfiles should not be created because we did not declare keepfiles on
# the original config
with self.assertRaises(AttributeError):
Expand Down

0 comments on commit c849905

Please sign in to comment.