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 18 commits
Commits
Show all changes
24 commits
Select commit Hold shift click to select a range
528a4f7
Adding infrastructure to support ipopt_v2
andrewlee94 Jun 14, 2024
0faabba
Moving core/util to ipopt_v2
andrewlee94 Jun 14, 2024
47d9d73
Moving MH initializer to ipopt_v2
andrewlee94 Jun 15, 2024
47beac6
Fixing pint version issue
andrewlee94 Jun 17, 2024
c39f256
Set TSA to use old IPOPT interface
andrewlee94 Jun 17, 2024
04f1463
Fixing conflict
andrewlee94 Jun 17, 2024
3ce08dd
Trying to resolve Windows failures
andrewlee94 Jun 17, 2024
4aa1405
Working on platofrm dependent failure
andrewlee94 Jun 17, 2024
7353472
BTInitializer with presolve
andrewlee94 Jun 20, 2024
e88bd83
Merge branch 'main' of https://github.com/IDAES/idaes-pse into presolve
andrewlee94 Jun 20, 2024
d5135f4
Moving last bits of core code to ipopt_v2
andrewlee94 Jun 20, 2024
9ccc8b4
Starting on idaes/models
andrewlee94 Jun 20, 2024
9423501
Removing ma57_automatic_scaling default and updating idaes/models/con…
andrewlee94 Jun 20, 2024
6754964
idaes/model/properties part 1
andrewlee94 Jun 20, 2024
f6079ca
Remaining parts of idaes/models/proeprties
andrewlee94 Jun 20, 2024
ad475fd
Fixing typo
andrewlee94 Jun 20, 2024
dda16b3
Switching idaes/models/unit_models to ipopt_v2
andrewlee94 Jun 21, 2024
bc8e411
Attempt to work around HXLC issues for now
andrewlee94 Jun 21, 2024
4273e24
Some clean up
andrewlee94 Jun 21, 2024
542914a
Switching modular properties initializer to solver indexed blocks
andrewlee94 Jun 21, 2024
6578d01
Merge branch 'modular_properties_init' into presolve
andrewlee94 Jun 21, 2024
84e9d69
Addressing comments
andrewlee94 Jun 21, 2024
6a66e13
Fixing pylint warings
andrewlee94 Jun 21, 2024
2df54c5
Removing unnecessary test for legacy solver wrapper
andrewlee94 Jun 26, 2024
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
88 changes: 88 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,93 @@ def _new_idaes_config_block():
),
)

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

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

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

cfg["ipopt_v2"]["options"].declare(
"max_iter",
pyomo.common.config.ConfigValue(
domain=int,
default=200,
description="Ipopt max_iter option",
doc="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",
doc="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'",
doc="Default writer configuration for 'ipopt' solver",
),
)

# TODO: Remember to update BTInitializer to sue get_solver once scaling tools are deployed.
andrewlee94 marked this conversation as resolved.
Show resolved Hide resolved
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",
doc="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",
doc="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
38 changes: 34 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,54 @@


# 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).

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