# ___________________________________________________________________________
#
# Pyomo: Python Optimization Modeling Objects
# Copyright (c) 2008-2024
# National Technology and Engineering Solutions of Sandia, LLC
# Under the terms of Contract DE-NA0003525 with National Technology and
# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain
# rights in this software.
#
# Development of this module was conducted as part of the Institute for
# the Design of Advanced Energy Systems (IDAES) with support through the
# Simulation-Based Engineering, Crosscutting Research Program within the
# U.S. Department of Energy’s Office of Fossil Energy and Carbon Management.
#
# This software is distributed under the 3-clause BSD License.
# ___________________________________________________________________________
import logging
from pyomo.core.base.range import NumericRange
from pyomo.common.config import (
ConfigDict,
ConfigValue,
Bool,
PositiveInt,
PositiveFloat,
In,
document_kwargs_from_configdict,
)
from pyomo.contrib.trustregion.filter import Filter, FilterElement
from pyomo.contrib.trustregion.interface import TRFInterface
from pyomo.contrib.trustregion.util import IterationLogger
from pyomo.opt import SolverFactory
logger = logging.getLogger('pyomo.contrib.trustregion')
__version__ = (0, 2, 0)
[docs]
def trust_region_method(model, decision_variables, ext_fcn_surrogate_map_rule, config):
"""
The main driver of the Trust Region algorithm method.
Parameters
----------
model : ConcreteModel
The user's model to be solved.
degrees_of_freedom_variables : List of Vars
User-supplied input. The user must provide a list of vars which
are the degrees of freedom or decision variables within
the model.
ext_fcn_surrogate_map_rule : Function, optional
In the 2020 Yoshio/Biegler paper, this is referred to as
the basis function `b(w)`.
This is the low-fidelity model with which to solve the original
process model problem and which is integrated into the
surrogate model.
The default is 0 (i.e., no basis function rule.)
config : ConfigDict
This holds the solver and TRF-specific configuration options.
"""
# Initialize necessary TRF methods
TRFLogger = IterationLogger()
TRFilter = Filter()
interface = TRFInterface(
model, decision_variables, ext_fcn_surrogate_map_rule, config
)
# Initialize the problem
rebuildSM = False
obj_val, feasibility = interface.initializeProblem()
# Initialize first iteration feasibility/objective value to enable
# termination check
feasibility_k = feasibility
obj_val_k = obj_val
# Initialize step_norm_k to a bogus value to enable termination check
step_norm_k = 0
# Initialize trust region radius
trust_radius = config.trust_radius
iteration = 0
TRFLogger.newIteration(
iteration, feasibility_k, obj_val_k, trust_radius, step_norm_k
)
TRFLogger.logIteration()
if config.verbose:
TRFLogger.printIteration()
while iteration < config.maximum_iterations:
iteration += 1
# Check termination conditions
if (feasibility_k <= config.feasibility_termination) and (
step_norm_k <= config.step_size_termination
):
print('EXIT: Optimal solution found.')
interface.model.display()
break
# If trust region very small and no progress is being made,
# terminate. The following condition must hold for two
# consecutive iterations.
if (trust_radius <= config.minimum_radius) and (
abs(feasibility_k - feasibility) < config.feasibility_termination
):
if subopt_flag:
logger.warning('WARNING: Insufficient progress.')
print('EXIT: Feasible solution found.')
break
else:
subopt_flag = True
else:
# This condition holds for iteration 0, which will declare
# the boolean subopt_flag
subopt_flag = False
# Set bounds to enforce the trust region
interface.updateDecisionVariableBounds(trust_radius)
# Generate suggorate model r_k(w)
if rebuildSM:
interface.updateSurrogateModel()
# Solve the Trust Region Subproblem (TRSP)
obj_val_k, step_norm_k, feasibility_k = interface.solveModel()
TRFLogger.newIteration(
iteration, feasibility_k, obj_val_k, trust_radius, step_norm_k
)
# Check filter acceptance
filterElement = FilterElement(obj_val_k, feasibility_k)
if not TRFilter.isAcceptable(filterElement, config.maximum_feasibility):
# Reject the step
TRFLogger.iterrecord.rejected = True
trust_radius = max(
config.minimum_radius, step_norm_k * config.radius_update_param_gamma_c
)
rebuildSM = False
interface.rejectStep()
# Log iteration information
TRFLogger.logIteration()
if config.verbose:
TRFLogger.printIteration()
continue
# Switching condition: Eq. (7) in Yoshio/Biegler (2020)
if (obj_val - obj_val_k) >= (
config.switch_condition_kappa_theta
* pow(feasibility, config.switch_condition_gamma_s)
) and (feasibility <= config.minimum_feasibility):
# f-type step
TRFLogger.iterrecord.fStep = True
trust_radius = min(
max(step_norm_k * config.radius_update_param_gamma_e, trust_radius),
config.maximum_radius,
)
else:
# theta-type step
TRFLogger.iterrecord.thetaStep = True
filterElement = FilterElement(
obj_val_k - config.param_filter_gamma_f * feasibility_k,
(1 - config.param_filter_gamma_theta) * feasibility_k,
)
TRFilter.addToFilter(filterElement)
# Calculate ratio: Eq. (10) in Yoshio/Biegler (2020)
rho_k = (
feasibility - feasibility_k + config.feasibility_termination
) / max(feasibility, config.feasibility_termination)
# Ratio tests: Eq. (8) in Yoshio/Biegler (2020)
# If rho_k is between eta_1 and eta_2, trust radius stays same
if (rho_k < config.ratio_test_param_eta_1) or (
feasibility > config.minimum_feasibility
):
trust_radius = max(
config.minimum_radius,
(config.radius_update_param_gamma_c * step_norm_k),
)
elif rho_k >= config.ratio_test_param_eta_2:
trust_radius = min(
config.maximum_radius,
max(
trust_radius, (config.radius_update_param_gamma_e * step_norm_k)
),
)
TRFLogger.updateIteration(trustRadius=trust_radius)
# Accept step and reset for next iteration
rebuildSM = True
feasibility = feasibility_k
obj_val = obj_val_k
# Log iteration information
TRFLogger.logIteration()
if config.verbose:
TRFLogger.printIteration()
if iteration >= config.maximum_iterations:
logger.warning(
'EXIT: Maximum iterations reached: {}.'.format(config.maximum_iterations)
)
return interface.model
def _trf_config():
"""
Generate the configuration dictionary.
The user may change the configuration options during the instantiation
of the trustregion solver:
>>> optTRF = SolverFactory('trustregion',
... solver='ipopt',
... maximum_iterations=50,
... minimum_radius=1e-5,
... verbose=True)
The user may also update the configuration after instantiation:
>>> optTRF = SolverFactory('trustregion')
>>> optTRF._CONFIG.trust_radius = 0.5
The user may also update the configuration as part of the solve call:
>>> optTRF = SolverFactory('trustregion')
>>> optTRF.solve(model, decision_variables, trust_radius=0.5)
Returns
-------
CONFIG : ConfigDict
This holds all configuration options to be passed to the TRF solver.
"""
CONFIG = ConfigDict('TrustRegion')
### Solver options
CONFIG.declare(
'solver',
ConfigValue(default='ipopt', description='Solver to use. Default = ``ipopt``.'),
)
CONFIG.declare(
'keepfiles',
ConfigValue(
default=False,
domain=Bool,
description="Optional. Whether or not to "
"write files of sub-problems for use in debugging. "
"Default = False.",
),
)
CONFIG.declare(
'tee',
ConfigValue(
default=False,
domain=Bool,
description="Optional. Sets the ``tee`` "
"for sub-solver(s) utilized. "
"Default = False.",
),
)
### Trust Region specific options
CONFIG.declare(
'verbose',
ConfigValue(
default=False,
domain=Bool,
description="Optional. When True, print each "
"iteration's relevant information to the console "
"as well as to the log. "
"Default = False.",
),
)
CONFIG.declare(
'trust_radius',
ConfigValue(
default=1.0,
domain=PositiveFloat,
description="Initial trust region radius ``delta_0``. Default = 1.0.",
),
)
CONFIG.declare(
'minimum_radius',
ConfigValue(
default=1e-6,
domain=PositiveFloat,
description="Minimum allowed trust region radius ``delta_min``. "
"Default = 1e-6.",
),
)
CONFIG.declare(
'maximum_radius',
ConfigValue(
default=CONFIG.trust_radius * 100,
domain=PositiveFloat,
description="Maximum allowed trust region radius. If trust region "
"radius reaches maximum allowed, solver will exit. "
"Default = 100 * trust_radius.",
),
)
CONFIG.declare(
'maximum_iterations',
ConfigValue(
default=50,
domain=PositiveInt,
description="Maximum allowed number of iterations. Default = 50.",
),
)
### Termination options
CONFIG.declare(
'feasibility_termination',
ConfigValue(
default=1e-5,
domain=PositiveFloat,
description="Feasibility measure termination tolerance ``epsilon_theta``. "
"Default = 1e-5.",
),
)
CONFIG.declare(
'step_size_termination',
ConfigValue(
default=CONFIG.feasibility_termination,
domain=PositiveFloat,
description="Step size termination tolerance ``epsilon_s``. "
"Matches the feasibility termination tolerance by default.",
),
)
### Switching Condition options
CONFIG.declare(
'minimum_feasibility',
ConfigValue(
default=1e-4,
domain=PositiveFloat,
description="Minimum feasibility measure ``theta_min``. Default = 1e-4.",
),
)
CONFIG.declare(
'switch_condition_kappa_theta',
ConfigValue(
default=0.1,
domain=In(NumericRange(0, 1, 0, (False, False))),
description="Switching condition parameter ``kappa_theta``. "
"Contained in open set (0, 1). "
"Default = 0.1.",
),
)
CONFIG.declare(
'switch_condition_gamma_s',
ConfigValue(
default=2.0,
domain=PositiveFloat,
description="Switching condition parameter ``gamma_s``. "
"Must satisfy: ``gamma_s > 1/(1+mu)`` where ``mu`` "
"is contained in set (0, 1]. "
"Default = 2.0.",
),
)
### Trust region update/ratio test parameters
CONFIG.declare(
'radius_update_param_gamma_c',
ConfigValue(
default=0.5,
domain=In(NumericRange(0, 1, 0, (False, False))),
description="Lower trust region update parameter ``gamma_c``. "
"Default = 0.5.",
),
)
CONFIG.declare(
'radius_update_param_gamma_e',
ConfigValue(
default=2.5,
domain=In(NumericRange(1, None, 0)),
description="Upper trust region update parameter ``gamma_e``. "
"Default = 2.5.",
),
)
CONFIG.declare(
'ratio_test_param_eta_1',
ConfigValue(
default=0.05,
domain=In(NumericRange(0, 1, 0, (False, False))),
description="Lower ratio test parameter ``eta_1``. "
"Must satisfy: ``0 < eta_1 <= eta_2 < 1``. "
"Default = 0.05.",
),
)
CONFIG.declare(
'ratio_test_param_eta_2',
ConfigValue(
default=0.2,
domain=In(NumericRange(0, 1, 0, (False, False))),
description="Lower ratio test parameter ``eta_2``. "
"Must satisfy: ``0 < eta_1 <= eta_2 < 1``. "
"Default = 0.2.",
),
)
### Filter
CONFIG.declare(
'maximum_feasibility',
ConfigValue(
default=50.0,
domain=PositiveFloat,
description="Maximum allowable feasibility measure ``theta_max``. "
"Parameter for use in filter method."
"Default = 50.0.",
),
)
CONFIG.declare(
'param_filter_gamma_theta',
ConfigValue(
default=0.01,
domain=In(NumericRange(0, 1, 0, (False, False))),
description="Fixed filter parameter ``gamma_theta`` within (0, 1). "
"Default = 0.01",
),
)
CONFIG.declare(
'param_filter_gamma_f',
ConfigValue(
default=0.01,
domain=In(NumericRange(0, 1, 0, (False, False))),
description="Fixed filter parameter ``gamma_f`` within (0, 1). "
"Default = 0.01",
),
)
return CONFIG
[docs]
@SolverFactory.register(
'trustregion',
doc='Trust region algorithm "solver" for black box/glass box optimization',
)
class TrustRegionSolver(object):
"""
The Trust Region Solver is a 'solver' based on the 2016/2018/2020 AiChE
papers by Eason (2016/2018), Yoshio (2020), and Biegler.
"""
CONFIG = _trf_config()
[docs]
def __init__(self, **kwds):
self.config = self.CONFIG(kwds)
[docs]
def available(self, exception_flag=True):
"""
Check if solver is available.
"""
return True
[docs]
def version(self):
"""
Return a 3-tuple describing the solver version.
"""
return __version__
[docs]
def license_is_valid(self):
"""
License for using Trust Region solver.
"""
return True
def __enter__(self):
return self
def __exit__(self, et, ev, tb):
pass
[docs]
@document_kwargs_from_configdict(CONFIG)
def solve(
self,
model,
degrees_of_freedom_variables,
ext_fcn_surrogate_map_rule=None,
**kwds
):
"""
This method calls the TRF algorithm.
Parameters
----------
model : ConcreteModel
The model to be solved using the Trust Region Framework.
degrees_of_freedom_variables : List[Var]
User-supplied input. The user must provide a list of vars which
are the degrees of freedom or decision variables within
the model.
ext_fcn_surrogate_map_rule : Function, optional
In the 2020 Yoshio/Biegler paper, this is referred to as
the basis function `b(w)`.
This is the low-fidelity model with which to solve the original
process model problem and which is integrated into the
surrogate model.
The default is 0 (i.e., no basis function rule.)
"""
config = self.config(kwds.pop('options', {}))
config.set_value(kwds)
if ext_fcn_surrogate_map_rule is None:
# If the user does not pass us a "basis" function,
# we default to 0.
ext_fcn_surrogate_map_rule = lambda comp, ef: 0
result = trust_region_method(
model, degrees_of_freedom_variables, ext_fcn_surrogate_map_rule, config
)
return result