Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Activating IPOPT_V2 with presolver #1436

Merged
merged 24 commits into from
Jun 26, 2024
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions idaes/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import os

import pyomo.common.config
from pyomo.common.config import Bool

_log = logging.getLogger(__name__)
# Default release version if no options provided for get-extensions
Expand Down Expand Up @@ -322,6 +323,84 @@ def _new_idaes_config_block():
),
)

cfg.declare(
"ipopt_v2",
pyomo.common.config.ConfigBlock(
implicit=False,
description="Default config for 'ipopt' solver",
),
)
cfg["ipopt_v2"].declare(
"options",
pyomo.common.config.ConfigBlock(
implicit=True,
description="Default solver options for 'ipopt'",
),
)

cfg["ipopt_v2"]["options"].declare(
"nlp_scaling_method",
pyomo.common.config.ConfigValue(
domain=str,
default="gradient-based",
description="Ipopt NLP scaling method",
),
)

cfg["ipopt_v2"]["options"].declare(
"tol",
pyomo.common.config.ConfigValue(
domain=float,
default=1e-6,
description="Ipopt tol option",
),
)

cfg["ipopt_v2"]["options"].declare(
"max_iter",
pyomo.common.config.ConfigValue(
domain=int,
default=200,
description="Ipopt max_iter option",
),
)

cfg["ipopt_v2"]["options"].declare(
"linear_solver",
pyomo.common.config.ConfigValue(
domain=str,
default="ma57",
description="Linear solver to be used by IPOPT",
),
)

cfg["ipopt_v2"].declare(
"writer_config",
pyomo.common.config.ConfigBlock(
implicit=True,
description="Default writer configuration for 'ipopt'",
),
)

# TODO: Remember to update BTInitializer to use get_solver once scaling tools are deployed.
cfg["ipopt_v2"]["writer_config"].declare(
"scale_model",
pyomo.common.config.ConfigValue(
domain=Bool,
default=False, # TODO: Change to true once transition complete
description="Whether to apply model scaling in writer",
),
)

cfg["ipopt_v2"]["writer_config"].declare(
"linear_presolve",
pyomo.common.config.ConfigValue(
domain=Bool,
default=True,
description="Whether to apply linear presolve in writer",
),
)

cfg.declare(
"ipopt_l1",
pyomo.common.config.ConfigBlock(
Expand Down
59 changes: 50 additions & 9 deletions idaes/core/initialization/block_triangularization.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
Initializer class for implementing Block Triangularization initialization
"""
from pyomo.environ import SolverFactory
from pyomo.common.config import ConfigDict, ConfigValue
from pyomo.common.config import Bool, ConfigDict, ConfigValue
from pyomo.contrib.incidence_analysis import (
IncidenceGraphInterface,
solve_strongly_connected_components,
Expand All @@ -25,7 +25,6 @@
InitializationStatus,
)
from idaes.core.util.exceptions import InitializationError
from idaes.core.solvers import get_solver

__author__ = "Andrew Lee"

Expand All @@ -47,7 +46,7 @@ class BlockTriangularizationInitializer(InitializerBase):
CONFIG.declare(
"block_solver",
ConfigValue(
default="ipopt",
default="ipopt_v2",
description="Solver to use for NxN blocks",
),
)
Expand All @@ -59,13 +58,52 @@ class BlockTriangularizationInitializer(InitializerBase):
doc="Dict of options to use to set solver.options.",
),
)
CONFIG.block_solver_options.declare(
"tol",
ConfigValue(
default=1e-8,
domain=float,
description="Convergence tolerance for block solver",
),
)
Comment on lines +61 to +68
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be good to set constr_viol_tol as well. I'm not sure what tol does in square problems, but in optimization problems constr_viol_tol controls the constraint violation.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whilst most tests appear to pass if we change this to 1e-6, there are a few that start failing so I will leave this out for now. Users can always set this value if they wish.

CONFIG.block_solver_options.declare(
"max_iter",
ConfigValue(
default=200,
domain=int,
description="Iteration limit for block solver",
),
)
CONFIG.declare(
"block_solver_writer_config",
ConfigDict(
implicit=True,
description="Dict of writer_config arguments to pass to block solver",
),
)
CONFIG.block_solver_writer_config.declare(
"linear_presolve",
ConfigValue(
default=True,
domain=Bool,
description="Whether to use linear presolver with block solver",
),
)
CONFIG.block_solver_writer_config.declare(
"scale_model",
ConfigValue(
default=False,
domain=Bool,
description="Whether to apply model scaling with block solver",
),
)
CONFIG.declare(
"block_solver_call_options",
ConfigDict(
implicit=True,
description="Dict of arguments to pass to solver.solve call",
doc="Dict of arguments to be passed as part of the solver.solve "
"call, such as tee=True/",
"call, such as tee=True.",
),
)
CONFIG.declare(
Expand Down Expand Up @@ -111,11 +149,14 @@ def initialization_routine(self, model):
"""
Call Block Triangularization solver on model.
"""
if self.config.block_solver is not None:
solver = SolverFactory(self.config.block_solver)
solver.options.update(self.config.block_solver_options)
else:
solver = get_solver(options=self.config.block_solver_options)
# TODO: For now, go directly through solver factory as default solver
# options cause failures. Most of these appear to be due to scaling,
# so hopefully we can fix these later.
solver = SolverFactory(
self.config.block_solver,
options=self.config.block_solver_options,
writer_config=self.config.block_solver_writer_config,
)

if model.is_indexed():
for d in model.values():
Expand Down
15 changes: 13 additions & 2 deletions idaes/core/initialization/initializer_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -551,7 +551,7 @@ class ModularInitializerBase(InitializerBase):
CONFIG.declare(
"solver",
ConfigValue(
default=None, # TODO: Can we add a square problem solver as the default here?
default="ipopt_v2", # TODO: Can we add a square problem solver as the default here?
# At the moment there is an issue with the scipy solvers not supporting the tee argument.
description="Solver to use for initialization",
),
Expand All @@ -563,6 +563,13 @@ class ModularInitializerBase(InitializerBase):
description="Dict of options to pass to solver",
),
)
CONFIG.declare(
"writer_config",
ConfigDict(
implicit=True,
description="Dict of writer_config arguments to pass to solver",
),
)
CONFIG.declare(
"default_submodel_initializer",
ConfigValue(
Expand Down Expand Up @@ -820,6 +827,10 @@ def cleanup(self, model, plugin_initializer_args, sub_initializers):

def _get_solver(self):
if self._solver is None:
self._solver = get_solver(self.config.solver, self.config.solver_options)
self._solver = get_solver(
self.config.solver,
solver_options=self.config.solver_options,
writer_config=self.config.writer_config,
)

return self._solver
3 changes: 2 additions & 1 deletion idaes/core/initialization/tests/test_initializer_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -706,8 +706,9 @@ def test_base_attributed(self):
assert initializer.config.default_submodel_initializer is None

assert initializer._solver is None
assert initializer.config.solver is None
assert initializer.config.solver == "ipopt_v2"
assert initializer.config.solver_options == {}
assert initializer.config.writer_config == {}

@pytest.mark.unit
def test_get_submodel_initializer_specific_model(self):
Expand Down
27 changes: 20 additions & 7 deletions idaes/core/solvers/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,22 @@
# All rights reserved. Please see the files COPYRIGHT.md and LICENSE.md
# for full copyright and license information.
#################################################################################
# TODO: Missing doc strings
# pylint: disable=missing-module-docstring
# pylint: disable=missing-class-docstring
"""
Wrapper for Pyomo solvers to allow us to define default solver options
"""

from copy import deepcopy

from pyomo.environ import SolverFactory

import idaes


class SolverWrapper(object):
"""
Wrapper for Pyomo solvers to allow us to define default solver options
"""

def __init__(self, name, register=True):
if name is None:
name = "default"
Expand All @@ -43,20 +50,26 @@ def __call__(self, *args, **kwargs):
name = self.name
solver = self.solver
if name in idaes.cfg and (
idaes.cfg.use_idaes_solver_config
or name == "default"
or not self.registered
idaes.cfg.use_idaes_solver_config or not self.registered
):
for k, v in idaes.cfg[name].items():
if k not in kwargs:
kwargs[k] = v
kwargs[k] = deepcopy(v)
elif k == "options":
# options is in ConfigBlock and in kwargs, treat "options"
# special so individual options can have defaults not just
# the whole options block
for opk, opv in v.items():
if opk not in kwargs["options"]:
kwargs["options"][opk] = opv
elif k == "writer_config":
# writer_config is in ConfigBlock and in kwargs, treat "writer_config"
# special so individual options can have defaults not just
# the whole options block
for opk, opv in v.items():
if opk not in kwargs["writer_config"]:
kwargs["writer_config"][opk] = opv

return solver(*args, **kwargs)


Expand Down
42 changes: 38 additions & 4 deletions idaes/core/solvers/get_solver.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"""
This module contains the IDAES get_solver method.
"""
from pyomo.contrib.solver.base import LegacySolverWrapper

import idaes.logger as idaeslog
import idaes.core.solvers
Expand All @@ -22,25 +23,58 @@


# Author: Andrew Lee
def get_solver(solver=None, options=None):
def get_solver(
solver=None,
solver_options: dict = None,
writer_config: dict = None,
options: dict = None,
):
"""
General method for getting a solver object which defaults to the standard
IDAES solver (defined in the IDAES configuration).

Args:
solver: string name for desired solver. Default=None, use default solver
options: dict of solver options to use, overwrites any settings
solver_options: dict of solver options to use, overwrites any settings
provided by IDAES configuration. Default = None, use default
solver options.
writer_config: dict of configuration options for solver writer, overwrites
ny settings provided by IDAES configuration. Default = None, use
default solver options.
options: DEPRECATED. Alias of solver_options.

Returns:
A Pyomo solver object
"""
if solver_options is not None:
if options is not None:
raise ValueError(
"Cannot provide both the 'options' and 'solver_options' argument. "
"'options' has been deprecated in favor of 'solver_options'."
)
options = solver_options

if solver is None:
solver = "default"
solver_obj = idaes.core.solvers.SolverWrapper(solver, register=False)()

if options is not None:
solver_obj.options.update(options)
if isinstance(solver_obj, LegacySolverWrapper):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is unfortunate that we need to have this test here: the whole point of the Legacy interface is that you shouldn't have to change your code. Is there a reason why you can't just do:

if options is not None:
    for k, v in options.items():
        solver_obj.options[k] = v
if writer_config is not None:
    for k, v in writer_config.items():
        solver_obj.config.writer_config[k] = v

for both new and old solvers? If there writer_config is not None for an old solver, it should just generate an error when you try to set the value on config.writer_config...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can definitely try (and this is why I was waiting for your review).

# New solver interface.
# LegacySolverWrapper is a wrapper for the new solver interface that makes it
# backward compatible.
if options is not None:
for k, v in options.items():
solver_obj.options[k] = v
if writer_config is not None:
for k, v in writer_config.items():
solver_obj.config.writer_config[k] = v
else:
# Old solver interface
if options is not None:
solver_obj.options.update(options)
if writer_config is not None:
_log.info(
"Older Pyomo solver interface does not support writer_config argument: ignoring."
)
andrewlee94 marked this conversation as resolved.
Show resolved Hide resolved

return solver_obj
Loading
Loading