# ___________________________________________________________________________
#
# 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.
# This software is distributed under the 3-clause BSD License.
# ___________________________________________________________________________
import io
import logging
import sys
from collections.abc import Sequence
from typing import Optional, List, TextIO
from pyomo.common.config import (
ConfigDict,
ConfigValue,
NonNegativeFloat,
NonNegativeInt,
ADVANCED_OPTION,
Bool,
Path,
)
from pyomo.common.log import LogStream
from pyomo.common.numeric_types import native_logical_types
from pyomo.common.timing import HierarchicalTimer
[docs]
def TextIO_or_Logger(val):
"""
Validates and converts input into a list of valid output streams.
Accepts:
- sys.stdout
- Instances of io.TextIOBase
- logging.Logger (wrapped as LogStream)
- Boolean values (`True` -> sys.stdout)
Returns:
- A list of validated output streams.
Raises:
- ValueError if an invalid type is provided.
"""
if isinstance(val, Sequence) and not isinstance(val, (str, bytes)):
val = list(val)
else:
val = [val]
ans = []
for v in val:
if v.__class__ in native_logical_types:
if v:
ans.append(sys.stdout)
elif isinstance(v, (sys.stdout.__class__, io.TextIOBase)):
# We are guarding against file-like classes that do not derive from
# TextIOBase but are assigned to stdout / stderr.
# We still want to accept those classes.
ans.append(v)
elif isinstance(v, logging.Logger):
ans.append(LogStream(level=logging.INFO, logger=v))
else:
raise ValueError(
f"Expected sys.stdout, io.TextIOBase, Logger, or bool, but received {v.__class__}"
)
return ans
[docs]
class SolverConfig(ConfigDict):
"""
Common configuration options for all solver interfaces
"""
[docs]
def __init__(
self,
description=None,
doc=None,
implicit=False,
implicit_domain=None,
visibility=0,
):
super().__init__(
description=description,
doc=doc,
implicit=implicit,
implicit_domain=implicit_domain,
visibility=visibility,
)
self.tee: List[TextIO] = self.declare(
'tee',
ConfigValue(
domain=TextIO_or_Logger,
default=False,
description="""``tee`` accepts :py:class:`bool`,
:py:class:`io.TextIOBase`, or :py:class:`logging.Logger`
(or a list of these types). ``True`` is mapped to
``sys.stdout``. The solver log will be printed to each of
these streams / destinations.""",
),
)
self.working_dir: Optional[Path] = self.declare(
'working_dir',
ConfigValue(
domain=Path(),
default=None,
description="The directory in which generated files should be saved. "
"This replaces the `keepfiles` option.",
),
)
self.load_solutions: bool = self.declare(
'load_solutions',
ConfigValue(
domain=Bool,
default=True,
description="If True, the values of the primal variables will be loaded into the model.",
),
)
self.raise_exception_on_nonoptimal_result: bool = self.declare(
'raise_exception_on_nonoptimal_result',
ConfigValue(
domain=Bool,
default=True,
description="If False, the `solve` method will continue processing "
"even if the returned result is nonoptimal.",
),
)
self.symbolic_solver_labels: bool = self.declare(
'symbolic_solver_labels',
ConfigValue(
domain=Bool,
default=False,
description="If True, the names given to the solver will reflect the names of the Pyomo components. "
"Cannot be changed after set_instance is called.",
),
)
self.timer: Optional[HierarchicalTimer] = self.declare(
'timer',
ConfigValue(
default=None,
description="A timer object for recording relevant process timing data.",
),
)
self.threads: Optional[int] = self.declare(
'threads',
ConfigValue(
domain=NonNegativeInt,
description="Number of threads to be used by a solver.",
default=None,
),
)
self.time_limit: Optional[float] = self.declare(
'time_limit',
ConfigValue(
domain=NonNegativeFloat,
description="Time limit applied to the solver (in seconds).",
),
)
self.solver_options: ConfigDict = self.declare(
'solver_options',
ConfigDict(implicit=True, description="Options to pass to the solver."),
)
[docs]
class BranchAndBoundConfig(SolverConfig):
"""
Base config for all direct MIP solver interfaces
Attributes
----------
rel_gap: float
The relative value of the gap in relation to the best bound
abs_gap: float
The absolute value of the difference between the incumbent and best bound
"""
[docs]
def __init__(
self,
description=None,
doc=None,
implicit=False,
implicit_domain=None,
visibility=0,
):
super().__init__(
description=description,
doc=doc,
implicit=implicit,
implicit_domain=implicit_domain,
visibility=visibility,
)
self.rel_gap: Optional[float] = self.declare(
'rel_gap',
ConfigValue(
domain=NonNegativeFloat,
description="Optional termination condition; the relative value of the "
"gap in relation to the best bound",
),
)
self.abs_gap: Optional[float] = self.declare(
'abs_gap',
ConfigValue(
domain=NonNegativeFloat,
description="Optional termination condition; the absolute value of the "
"difference between the incumbent and best bound",
),
)
[docs]
class AutoUpdateConfig(ConfigDict):
"""
This is necessary for persistent solvers.
Attributes
----------
check_for_new_or_removed_constraints: bool
check_for_new_or_removed_vars: bool
check_for_new_or_removed_params: bool
check_for_new_objective: bool
update_constraints: bool
update_vars: bool
update_parameters: bool
update_named_expressions: bool
update_objective: bool
treat_fixed_vars_as_params: bool
"""
[docs]
def __init__(
self,
description=None,
doc=None,
implicit=False,
implicit_domain=None,
visibility=0,
):
if doc is None:
doc = 'Configuration options to detect changes in model between solves'
super().__init__(
description=description,
doc=doc,
implicit=implicit,
implicit_domain=implicit_domain,
visibility=visibility,
)
self.check_for_new_or_removed_constraints: bool = self.declare(
'check_for_new_or_removed_constraints',
ConfigValue(
domain=bool,
default=True,
description="""
If False, new/old constraints will not be automatically detected on subsequent
solves. Use False only when manually updating the solver with opt.add_constraints()
and opt.remove_constraints() or when you are certain constraints are not being
added to/removed from the model.""",
),
)
self.check_for_new_or_removed_vars: bool = self.declare(
'check_for_new_or_removed_vars',
ConfigValue(
domain=bool,
default=True,
description="""
If False, new/old variables will not be automatically detected on subsequent
solves. Use False only when manually updating the solver with opt.add_variables() and
opt.remove_variables() or when you are certain variables are not being added to /
removed from the model.""",
),
)
self.check_for_new_or_removed_params: bool = self.declare(
'check_for_new_or_removed_params',
ConfigValue(
domain=bool,
default=True,
description="""
If False, new/old parameters will not be automatically detected on subsequent
solves. Use False only when manually updating the solver with opt.add_parameters() and
opt.remove_parameters() or when you are certain parameters are not being added to /
removed from the model.""",
),
)
self.check_for_new_objective: bool = self.declare(
'check_for_new_objective',
ConfigValue(
domain=bool,
default=True,
description="""
If False, new/old objectives will not be automatically detected on subsequent
solves. Use False only when manually updating the solver with opt.set_objective() or
when you are certain objectives are not being added to / removed from the model.""",
),
)
self.update_constraints: bool = self.declare(
'update_constraints',
ConfigValue(
domain=bool,
default=True,
description="""
If False, changes to existing constraints will not be automatically detected on
subsequent solves. This includes changes to the lower, body, and upper attributes of
constraints. Use False only when manually updating the solver with
opt.remove_constraints() and opt.add_constraints() or when you are certain constraints
are not being modified.""",
),
)
self.update_vars: bool = self.declare(
'update_vars',
ConfigValue(
domain=bool,
default=True,
description="""
If False, changes to existing variables will not be automatically detected on
subsequent solves. This includes changes to the lb, ub, domain, and fixed
attributes of variables. Use False only when manually updating the solver with
opt.update_variables() or when you are certain variables are not being modified.""",
),
)
self.update_parameters: bool = self.declare(
'update_parameters',
ConfigValue(
domain=bool,
default=True,
description="""
If False, changes to parameter values will not be automatically detected on
subsequent solves. Use False only when manually updating the solver with
opt.update_parameters() or when you are certain parameters are not being modified.""",
),
)
self.update_named_expressions: bool = self.declare(
'update_named_expressions',
ConfigValue(
domain=bool,
default=True,
description="""
If False, changes to Expressions will not be automatically detected on
subsequent solves. Use False only when manually updating the solver with
opt.remove_constraints() and opt.add_constraints() or when you are certain
Expressions are not being modified.""",
),
)
self.update_objective: bool = self.declare(
'update_objective',
ConfigValue(
domain=bool,
default=True,
description="""
If False, changes to objectives will not be automatically detected on
subsequent solves. This includes the expr and sense attributes of objectives. Use
False only when manually updating the solver with opt.set_objective() or when you are
certain objectives are not being modified.""",
),
)
self.treat_fixed_vars_as_params: bool = self.declare(
'treat_fixed_vars_as_params',
ConfigValue(
domain=bool,
default=True,
visibility=ADVANCED_OPTION,
description="""
This is an advanced option that should only be used in special circumstances.
With the default setting of True, fixed variables will be treated like parameters.
This means that z == x*y will be linear if x or y is fixed and the constraint
can be written to an LP file. If the value of the fixed variable gets changed, we have
to completely reprocess all constraints using that variable. If
treat_fixed_vars_as_params is False, then constraints will be processed as if fixed
variables are not fixed, and the solver will be told the variable is fixed. This means
z == x*y could not be written to an LP file even if x and/or y is fixed. However,
updating the values of fixed variables is much faster this way.""",
),
)
[docs]
class PersistentSolverConfig(SolverConfig):
"""
Base config for all persistent solver interfaces
"""
[docs]
def __init__(
self,
description=None,
doc=None,
implicit=False,
implicit_domain=None,
visibility=0,
):
super().__init__(
description=description,
doc=doc,
implicit=implicit,
implicit_domain=implicit_domain,
visibility=visibility,
)
self.auto_updates: AutoUpdateConfig = self.declare(
'auto_updates', AutoUpdateConfig()
)
[docs]
class PersistentBranchAndBoundConfig(BranchAndBoundConfig):
"""
Base config for all persistent MIP solver interfaces
"""
[docs]
def __init__(
self,
description=None,
doc=None,
implicit=False,
implicit_domain=None,
visibility=0,
):
super().__init__(
description=description,
doc=doc,
implicit=implicit,
implicit_domain=implicit_domain,
visibility=visibility,
)
self.auto_updates: AutoUpdateConfig = self.declare(
'auto_updates', AutoUpdateConfig()
)