From 3121fa16fc192df2e87fad2c7c6ff5998faa26c6 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Fri, 27 Oct 2023 14:14:55 -0600 Subject: [PATCH 001/128] moving coramin to contrib --- pyomo/contrib/coramin/README.md | 43 + pyomo/contrib/coramin/__init__.py | 13 + pyomo/contrib/coramin/algorithms/__init__.py | 2 + .../contrib/coramin/algorithms/ecp_bounder.py | 331 ++++ .../coramin/algorithms/multitree/__init__.py | 0 .../coramin/algorithms/multitree/multitree.py | 1001 +++++++++++ .../algorithms/multitree/tests/__init__.py | 0 .../multitree/tests/test_multitree.py | 278 +++ .../algorithms/tests/test_ecp_bounder.py | 25 + pyomo/contrib/coramin/clone.py | 60 + .../coramin/domain_reduction/__init__.py | 8 + pyomo/contrib/coramin/domain_reduction/dbt.py | 1542 +++++++++++++++++ .../coramin/domain_reduction/filters.py | 193 +++ .../contrib/coramin/domain_reduction/obbt.py | 522 ++++++ .../domain_reduction/tests/test_dbt.py | 825 +++++++++ .../domain_reduction/tests/test_filters.py | 32 + .../domain_reduction/tests/test_obbt.py | 113 ++ pyomo/contrib/coramin/examples/alpha_bb.py | 50 + pyomo/contrib/coramin/examples/dbt.py | 90 + pyomo/contrib/coramin/examples/dbt2.py | 80 + pyomo/contrib/coramin/examples/ex.py | 86 + pyomo/contrib/coramin/examples/rosenbrock.py | 63 + pyomo/contrib/coramin/relaxations/__init__.py | 12 + pyomo/contrib/coramin/relaxations/_utils.py | 75 + pyomo/contrib/coramin/relaxations/alphabb.py | 166 ++ .../contrib/coramin/relaxations/auto_relax.py | 1381 +++++++++++++++ .../coramin/relaxations/copy_relaxation.py | 123 ++ .../coramin/relaxations/custom_block.py | 83 + pyomo/contrib/coramin/relaxations/hessian.py | 273 +++ .../contrib/coramin/relaxations/iterators.py | 95 + .../contrib/coramin/relaxations/mccormick.py | 307 ++++ .../coramin/relaxations/multivariate.py | 93 + .../coramin/relaxations/relaxations_base.py | 746 ++++++++ pyomo/contrib/coramin/relaxations/segments.py | 24 + .../contrib/coramin/relaxations/split_expr.py | 177 ++ .../coramin/relaxations/tests/test_alphabb.py | 59 + .../relaxations/tests/test_auto_relax.py | 1112 ++++++++++++ .../coramin/relaxations/tests/test_copy.py | 206 +++ .../relaxations/tests/test_iterators.py | 87 + .../relaxations/tests/test_mccormick.py | 102 ++ .../relaxations/tests/test_relaxations.py | 951 ++++++++++ .../tests/test_relaxations_base.py | 161 ++ .../tests/test_univariate_relaxations.py | 274 +++ .../contrib/coramin/relaxations/univariate.py | 1060 +++++++++++ pyomo/contrib/coramin/third_party/__init__.py | 1 + .../coramin/third_party/minlplib_tools.py | 349 ++++ .../third_party/tests/test_minlplib_tools.py | 206 +++ pyomo/contrib/coramin/utils/__init__.py | 2 + pyomo/contrib/coramin/utils/coramin_enums.py | 30 + pyomo/contrib/coramin/utils/mpi_utils.py | 111 ++ .../contrib/coramin/utils/plot_relaxation.py | 197 +++ pyomo/contrib/coramin/utils/pyomo_utils.py | 57 + 52 files changed, 13877 insertions(+) create mode 100644 pyomo/contrib/coramin/README.md create mode 100644 pyomo/contrib/coramin/__init__.py create mode 100644 pyomo/contrib/coramin/algorithms/__init__.py create mode 100644 pyomo/contrib/coramin/algorithms/ecp_bounder.py create mode 100644 pyomo/contrib/coramin/algorithms/multitree/__init__.py create mode 100644 pyomo/contrib/coramin/algorithms/multitree/multitree.py create mode 100644 pyomo/contrib/coramin/algorithms/multitree/tests/__init__.py create mode 100644 pyomo/contrib/coramin/algorithms/multitree/tests/test_multitree.py create mode 100644 pyomo/contrib/coramin/algorithms/tests/test_ecp_bounder.py create mode 100644 pyomo/contrib/coramin/clone.py create mode 100644 pyomo/contrib/coramin/domain_reduction/__init__.py create mode 100644 pyomo/contrib/coramin/domain_reduction/dbt.py create mode 100644 pyomo/contrib/coramin/domain_reduction/filters.py create mode 100644 pyomo/contrib/coramin/domain_reduction/obbt.py create mode 100644 pyomo/contrib/coramin/domain_reduction/tests/test_dbt.py create mode 100644 pyomo/contrib/coramin/domain_reduction/tests/test_filters.py create mode 100644 pyomo/contrib/coramin/domain_reduction/tests/test_obbt.py create mode 100644 pyomo/contrib/coramin/examples/alpha_bb.py create mode 100644 pyomo/contrib/coramin/examples/dbt.py create mode 100644 pyomo/contrib/coramin/examples/dbt2.py create mode 100644 pyomo/contrib/coramin/examples/ex.py create mode 100644 pyomo/contrib/coramin/examples/rosenbrock.py create mode 100644 pyomo/contrib/coramin/relaxations/__init__.py create mode 100644 pyomo/contrib/coramin/relaxations/_utils.py create mode 100644 pyomo/contrib/coramin/relaxations/alphabb.py create mode 100644 pyomo/contrib/coramin/relaxations/auto_relax.py create mode 100644 pyomo/contrib/coramin/relaxations/copy_relaxation.py create mode 100644 pyomo/contrib/coramin/relaxations/custom_block.py create mode 100644 pyomo/contrib/coramin/relaxations/hessian.py create mode 100644 pyomo/contrib/coramin/relaxations/iterators.py create mode 100644 pyomo/contrib/coramin/relaxations/mccormick.py create mode 100644 pyomo/contrib/coramin/relaxations/multivariate.py create mode 100644 pyomo/contrib/coramin/relaxations/relaxations_base.py create mode 100644 pyomo/contrib/coramin/relaxations/segments.py create mode 100644 pyomo/contrib/coramin/relaxations/split_expr.py create mode 100644 pyomo/contrib/coramin/relaxations/tests/test_alphabb.py create mode 100644 pyomo/contrib/coramin/relaxations/tests/test_auto_relax.py create mode 100644 pyomo/contrib/coramin/relaxations/tests/test_copy.py create mode 100644 pyomo/contrib/coramin/relaxations/tests/test_iterators.py create mode 100644 pyomo/contrib/coramin/relaxations/tests/test_mccormick.py create mode 100644 pyomo/contrib/coramin/relaxations/tests/test_relaxations.py create mode 100644 pyomo/contrib/coramin/relaxations/tests/test_relaxations_base.py create mode 100644 pyomo/contrib/coramin/relaxations/tests/test_univariate_relaxations.py create mode 100644 pyomo/contrib/coramin/relaxations/univariate.py create mode 100644 pyomo/contrib/coramin/third_party/__init__.py create mode 100644 pyomo/contrib/coramin/third_party/minlplib_tools.py create mode 100644 pyomo/contrib/coramin/third_party/tests/test_minlplib_tools.py create mode 100644 pyomo/contrib/coramin/utils/__init__.py create mode 100644 pyomo/contrib/coramin/utils/coramin_enums.py create mode 100644 pyomo/contrib/coramin/utils/mpi_utils.py create mode 100644 pyomo/contrib/coramin/utils/plot_relaxation.py create mode 100644 pyomo/contrib/coramin/utils/pyomo_utils.py diff --git a/pyomo/contrib/coramin/README.md b/pyomo/contrib/coramin/README.md new file mode 100644 index 00000000000..1a567666b19 --- /dev/null +++ b/pyomo/contrib/coramin/README.md @@ -0,0 +1,43 @@ +# Coramin + +Coramin is a Pyomo-based Python package that provides tools for +developing tailored algorithms for mixed-integer nonlinear programming +problems (MINLP's). This software includes classes for managing and +refining convex relaxations of nonconvex constraints. These classes +provide methods for updating the relaxation based on new variable +bounds, creating and managing piecewise relaxations (for multi-tree +based algorithms), and adding outer-approximation based cuts for +convex or concave constraints. These classes inherit from Pyomo +Blocks, so they can be easily integrated with Pyomo +models. Additionally, Coramin has functions for automatically +generating convex relaxations of general Pyomo models. Coramin also +has tools for domain reduction, including a parallel implementation +of optimization-based bounds tightening (OBBT) and various OBBT +filtering techniques. + +## Primary Contributors +### [Michael Bynum](https://github.com/michaelbynum) +- Relaxation classes +- OBBT +- OBBT Filtering +- Factorable programming approach to generating relaxations + +### [Carl Laird](https://github.com/carldlaird) +- Parallel OBBT +- McCormick and piecewise McCormick relaxations for bilinear terms +- Relaxations for univariate convex/concave fucntions + +### [Anya Castillo](https://github.com/anyacastillo) +- Relaxation classes + +### [Francesco Ceccon](https://github.com/fracek) +- Alpha-BB relaxation + +## Relevant Packages + +### [Suspect](https://github.com/cog-imperial/suspect) +Use of Coramin can be improved significantly by also utilizing +Suspect's convexity detection and feasibility-based bounds tightening +features. Future development of Coramin will directly use Suspect in +Coramin's factorable programming approach to generating relaxations. + diff --git a/pyomo/contrib/coramin/__init__.py b/pyomo/contrib/coramin/__init__.py new file mode 100644 index 00000000000..a191ae8bca8 --- /dev/null +++ b/pyomo/contrib/coramin/__init__.py @@ -0,0 +1,13 @@ +from coramin import utils +from coramin import domain_reduction +from coramin import relaxations +from coramin import algorithms +from coramin import third_party +from .utils import ( + RelaxationSide, + FunctionShape, + Effort, + EigenValueBounder, + simplify_expr, + get_objective +) diff --git a/pyomo/contrib/coramin/algorithms/__init__.py b/pyomo/contrib/coramin/algorithms/__init__.py new file mode 100644 index 00000000000..14e97b59fa7 --- /dev/null +++ b/pyomo/contrib/coramin/algorithms/__init__.py @@ -0,0 +1,2 @@ +from .ecp_bounder import ECPBounder +from .multitree.multitree import MultiTree diff --git a/pyomo/contrib/coramin/algorithms/ecp_bounder.py b/pyomo/contrib/coramin/algorithms/ecp_bounder.py new file mode 100644 index 00000000000..fc3177ed7b9 --- /dev/null +++ b/pyomo/contrib/coramin/algorithms/ecp_bounder.py @@ -0,0 +1,331 @@ +from pyomo.contrib import appsi +from pyomo.common.collections import ComponentSet +from coramin.utils.coramin_enums import RelaxationSide +import pyomo.environ as pe +import time +from pyomo.common.config import ConfigValue, NonNegativeInt, NonNegativeFloat, In +from pyomo.common.timing import HierarchicalTimer +from pyomo.core.staleflag import StaleFlagManager +from coramin.utils import get_objective +from coramin.relaxations import relaxation_data_objects +from typing import Optional +from typing import Sequence, Mapping, MutableMapping, Tuple, List +from pyomo.core.base.var import _GeneralVarData +from pyomo.core.base.constraint import _GeneralConstraintData +from pyomo.core.base.param import _ParamData + + +import logging + +logger = logging.getLogger(__name__) + + +class ECPConfig(appsi.base.SolverConfig): + def __init__( + self, + description=None, + doc=None, + implicit=False, + implicit_domain=None, + visibility=0, + ): + super(ECPConfig, self).__init__( + description=description, + doc=doc, + implicit=implicit, + implicit_domain=implicit_domain, + visibility=visibility, + ) + + self.feasibility_tol = self.declare( + "feasibility_tol", + ConfigValue( + default=1e-6, + domain=NonNegativeFloat, + doc="Tolerance below which cuts will not be added", + ), + ) + self.max_iter = self.declare( + "max_iter", + ConfigValue( + default=30, domain=NonNegativeInt, doc="Maximum number of iterations" + ), + ) + self.keep_cuts = self.declare( + "keep_cuts", + ConfigValue( + default=False, + domain=In([True, False]), + doc="Whether or not to keep the cuts generated after the solve", + ), + ) + + self.time_limit = 600 + + +class ECPSolutionLoader(appsi.base.SolutionLoaderBase): + def __init__(self, primals: MutableMapping[_GeneralVarData, float]): + self._primals = primals + + def get_primals( + self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None + ) -> Mapping[_GeneralVarData, float]: + if vars_to_load is None: + primals = pe.ComponentMap(self._primals) + else: + primals = pe.ComponentMap() + for v in vars_to_load: + primals[v] = self._primals[v] + return primals + + +class ECPResults(appsi.base.Results): + def __init__(self): + super(ECPResults, self).__init__() + self.wallclock_time = None + + +class ECPBounder(appsi.base.PersistentSolver): + """ + A solver designed for use inside of OBBT. This solver is a persistent solver for + efficient changes to the objective. Additionally, it provides a mechanism for + refining convex nonlinear constraints during OBBT. + """ + + def __init__(self, subproblem_solver: appsi.base.PersistentSolver): + super(ECPBounder, self).__init__() + self._subproblem_solver = subproblem_solver + self._relaxations = ComponentSet() + self._relaxations_with_added_cuts = ComponentSet() + self._pyomo_model = None + self._config = ECPConfig() + self._start_time: Optional[float] = None + self._update_config = appsi.base.UpdateConfig() + + def available(self): + return self._subproblem_solver.available() + + def version(self) -> Tuple: + return 0, 1, 0 + + @property + def symbol_map(self): + raise NotImplementedError("ECPBounder does not use a symbol map") + + @property + def config(self): + return self._config + + @config.setter + def config(self, val: ECPConfig): + self._config = val + + @property + def update_config(self) -> appsi.base.UpdateConfig: + return self._update_config + + @update_config.setter + def update_config(self, val: appsi.base.UpdateConfig): + self._update_config = val + + @property + def _elapsed_time(self): + return time.time() - self._start_time + + @property + def _remaining_time(self): + return max(0.0, self.config.time_limit - self._elapsed_time) + + def solve(self, model, timer: HierarchicalTimer = None) -> ECPResults: + self._start_time = time.time() + if timer is None: + timer = HierarchicalTimer() + timer.start("ECP Solve") + StaleFlagManager.mark_all_as_stale() + logger.info( + "{0:<10}{1:<12}{2:<12}{3:<12}{4:<12}".format( + "Iter", "objective", "max_viol", "time", "# cuts" + ) + ) + self._pyomo_model = model + + obj = get_objective(model) + if obj is None: + raise ValueError("Could not find any active objectives") + + final_res = ECPResults() + self._relaxations = ComponentSet() + self._relaxations_with_added_cuts = ComponentSet() + for b in relaxation_data_objects( + self._pyomo_model, descend_into=True, active=True + ): + self._relaxations.add(b) + + self._subproblem_solver.config.load_solution = False + orig_var_vals = pe.ComponentMap() + for v in self._pyomo_model.component_data_objects(pe.Var, descend_into=True): + orig_var_vals[v] = v.value + + all_added_cons = list() + for _iter in range(self.config.max_iter): + if self._elapsed_time >= self.config.time_limit: + final_res.termination_condition = ( + appsi.base.TerminationCondition.maxTimeLimit + ) + logger.warning("ECPBounder: time limit reached.") + break + self._subproblem_solver.config.time_limit = self._remaining_time + res = self._subproblem_solver.solve(self._pyomo_model, timer=timer) + if res.termination_condition == appsi.base.TerminationCondition.optimal: + res.solution_loader.load_vars() + else: + final_res.termination_condition = res.termination_condition + logger.warning("ECPBounder: subproblem did not terminate optimally") + break + + new_con_list = list() + max_viol = 0 + for b in self._relaxations: + viol = None + try: + if b.is_rhs_convex() and b.relaxation_side in { + RelaxationSide.BOTH, + RelaxationSide.UNDER, + }: + viol = pe.value(b.get_rhs_expr()) - b.get_aux_var().value + elif b.is_rhs_concave() and b.relaxation_side in { + RelaxationSide.BOTH, + RelaxationSide.OVER, + }: + viol = b.get_aux_var().value - pe.value(b.get_rhs_expr()) + except (OverflowError, ZeroDivisionError, ValueError) as err: + logger.warning("could not generate ECP cut due to " + str(err)) + if viol is not None: + if viol > max_viol: + max_viol = viol + if viol > self.config.feasibility_tol: + new_con = b.add_cut( + keep_cut=self.config.keep_cuts, + check_violation=True, + feasibility_tol=self.config.feasibility_tol, + ) + if new_con is not None: + self._relaxations_with_added_cuts.add(b) + new_con_list.append(new_con) + self._subproblem_solver.add_constraints(new_con_list) + + final_res.best_objective_bound = res.best_objective_bound + logger.info( + "{0:<10d}{1:<12.3e}{2:<12.3e}{3:<12.3e}{4:<12d}".format( + _iter, + final_res.best_objective_bound, + max_viol, + self._elapsed_time, + len(new_con_list), + ) + ) + + all_added_cons.extend(new_con_list) + + if len(new_con_list) == 0: + # The goal of the ECPBounder is not to find the optimal solution. + # Rather, the goal is just to get a decent bound quickly. + # However, if the problem is convex, we may still be able to declare + # optimality + final_res.termination_condition = ( + appsi.base.TerminationCondition.unknown + ) + logger.info("ECPBounder: converged!") + + found_feasible_solution = True + for b in self._relaxations: + deviation = b.get_deviation() + if deviation > self.config.feasibility_tol: + found_feasible_solution = False + + if found_feasible_solution: + final_res.termination_condition = ( + appsi.base.TerminationCondition.optimal + ) + final_res.best_feasible_objective = final_res.best_objective_bound + primal_sol = res.solution_loader.get_primals() + final_res.solution_loader = ECPSolutionLoader(primal_sol) + + break + + if _iter == self.config.max_iter - 1: + final_res.termination_condition = ( + appsi.base.TerminationCondition.maxIterations + ) + logger.warning("ECPBounder: reached maximum number of iterations") + + if not self.config.keep_cuts: + self._subproblem_solver.remove_constraints(all_added_cons) + for b in self._relaxations_with_added_cuts: + b.rebuild() + + if final_res.termination_condition == appsi.base.TerminationCondition.optimal: + if not self.config.load_solution: + for v, val in orig_var_vals.items(): + v.value = val + else: + if self.config.load_solution: + raise RuntimeError( + "A feasible solution was not found, so no solution can be loaded. " + "Please set opt.config.load_solution=False and check " + "results.termination_condition and results.best_feasible_objective " + "before loading a solution." + ) + for v, val in orig_var_vals.items(): + v.value = val + + final_res.wallclock_time = self._elapsed_time + timer.stop("ECP Solve") + return final_res + + def set_instance(self, model): + saved_update_config = self.update_config + saved_config = self.config + self.__init__(self._subproblem_solver) + self.config = saved_config + self.update_config = saved_update_config + self._pyomo_model = model + self._subproblem_solver.set_instance(model) + + def get_primals( + self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None + ) -> Mapping[_GeneralVarData, float]: + return self._subproblem_solver.get_primals(vars_to_load=vars_to_load) + + def add_block(self, block): + self._subproblem_solver.add_block(block) + + def add_constraints(self, cons: List[_GeneralConstraintData]): + self._subproblem_solver.add_constraints(cons) + + def add_variables(self, variables: List[_GeneralVarData]): + self._subproblem_solver.add_variables(variables=variables) + + def add_params(self, params: List[_ParamData]): + self._subproblem_solver.add_params(params=params) + + def remove_block(self, block): + self._subproblem_solver.remove_block(block) + + def remove_constraints(self, cons: List[_GeneralConstraintData]): + self._subproblem_solver.remove_constraints(cons=cons) + + def remove_variables(self, variables: List[_GeneralVarData]): + self._subproblem_solver.remove_variables(variables=variables) + + def remove_params(self, params: List[_ParamData]): + self._subproblem_solver.remove_params(params=params) + + def set_objective(self, obj): + self._subproblem_solver.set_objective(obj) + + def update_variables(self, variables: List[_GeneralVarData]): + self._subproblem_solver.update_variables(variables=variables) + + def update_params(self): + return self._subproblem_solver.update_params() diff --git a/pyomo/contrib/coramin/algorithms/multitree/__init__.py b/pyomo/contrib/coramin/algorithms/multitree/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/pyomo/contrib/coramin/algorithms/multitree/multitree.py b/pyomo/contrib/coramin/algorithms/multitree/multitree.py new file mode 100644 index 00000000000..acab6d0836f --- /dev/null +++ b/pyomo/contrib/coramin/algorithms/multitree/multitree.py @@ -0,0 +1,1001 @@ +import math +from coramin.relaxations.relaxations_base import ( + BaseRelaxationData, + BasePWRelaxationData, +) +import pyomo.environ as pe +from pyomo.common.timing import HierarchicalTimer +from pyomo.core.base.block import _BlockData +from pyomo.contrib.appsi.base import ( + Results, + PersistentSolver, + Solver, + MIPSolverConfig, + TerminationCondition, + SolutionLoaderBase, + UpdateConfig, +) +from pyomo.contrib import appsi +from typing import Tuple, Optional, MutableMapping, Sequence +from pyomo.common.config import ( + ConfigValue, NonNegativeInt, PositiveFloat, PositiveInt, NonNegativeFloat, InEnum +) +import logging +from coramin.relaxations.auto_relax import relax +from coramin.relaxations.iterators import relaxation_data_objects +from coramin.utils.coramin_enums import RelaxationSide, Effort, EigenValueBounder +from coramin.domain_reduction.dbt import push_integers, pop_integers, collect_vars_to_tighten +from coramin.domain_reduction.obbt import perform_obbt +import time +from pyomo.core.base.var import _GeneralVarData +from pyomo.core.base.objective import _GeneralObjectiveData +from coramin.utils.pyomo_utils import get_objective, active_vars +from pyomo.common.collections.component_set import ComponentSet +from pyomo.common.modeling import unique_component_name +from pyomo.common.errors import InfeasibleConstraintException +from pyomo.contrib.fbbt.fbbt import BoundsManager +import numpy as np +from pyomo.core.expr.visitor import identify_variables +from coramin.clone import clone_active_flat + + +logger = logging.getLogger(__name__) + + +class MultiTreeConfig(MIPSolverConfig): + def __init__( + self, + description=None, + doc=None, + implicit=False, + implicit_domain=None, + visibility=0, + ): + super(MultiTreeConfig, self).__init__( + description=description, + doc=doc, + implicit=implicit, + implicit_domain=implicit_domain, + visibility=visibility, + ) + + self.declare("solver_output_logger", ConfigValue()) + self.declare("log_level", ConfigValue(domain=NonNegativeInt)) + self.declare("feasibility_tolerance", ConfigValue(domain=PositiveFloat)) + self.declare("abs_gap", ConfigValue(domain=PositiveFloat)) + self.declare("max_partitions_per_iter", ConfigValue(domain=PositiveInt)) + self.declare("max_iter", ConfigValue(domain=NonNegativeInt)) + self.declare("root_obbt_max_iter", ConfigValue(domain=NonNegativeInt)) + self.declare("show_obbt_progress_bar", ConfigValue(domain=bool)) + self.declare("integer_tolerance", ConfigValue(domain=PositiveFloat)) + self.declare("small_coef", ConfigValue(domain=NonNegativeFloat)) + self.declare("large_coef", ConfigValue(domain=NonNegativeFloat)) + self.declare("safety_tol", ConfigValue(domain=NonNegativeFloat)) + self.declare("convexity_effort", ConfigValue(domain=InEnum(Effort))) + self.declare("obbt_at_new_incumbents", ConfigValue(domain=bool)) + self.declare("relax_integers_for_obbt", ConfigValue(domain=bool)) + + self.solver_output_logger = logger + self.log_level = logging.INFO + self.feasibility_tolerance = 1e-6 + self.integer_tolerance = 1e-4 + self.time_limit = 600 + self.abs_gap = 1e-4 + self.mip_gap = 0.001 + self.max_partitions_per_iter = 5 + self.max_iter = 1000 + self.root_obbt_max_iter = 1000 + self.show_obbt_progress_bar = False + self.small_coef = 1e-10 + self.large_coef = 1e5 + self.safety_tol = 1e-10 + self.convexity_effort = Effort.high + self.obbt_at_new_incumbents: bool = True + self.relax_integers_for_obbt: bool = True + + +def _is_problem_definitely_convex(m: _BlockData) -> bool: + res = True + for r in relaxation_data_objects(m, descend_into=True, active=True): + if r.relaxation_side == RelaxationSide.BOTH: + res = False + break + elif r.relaxation_side == RelaxationSide.UNDER and not r.is_rhs_convex(): + res = False + break + elif r.relaxation_side == RelaxationSide.OVER and not r.is_rhs_concave(): + res = False + break + return res + + +class MultiTreeResults(Results): + def __init__(self): + super().__init__() + self.wallclock_time = None + + +class MultiTreeSolutionLoader(SolutionLoaderBase): + def __init__(self, primals: MutableMapping): + self._primals = primals + + def get_primals( + self, vars_to_load: Optional[Sequence[_GeneralVarData]] = None + ) -> MutableMapping[_GeneralVarData, float]: + if vars_to_load is None: + return pe.ComponentMap(self._primals.items()) + else: + primals = pe.ComponentMap() + for v in vars_to_load: + primals[v] = self._primals[v] + return primals + + +class MultiTree(Solver): + def __init__(self, mip_solver: PersistentSolver, nlp_solver: PersistentSolver): + super(MultiTree, self).__init__() + self._config = MultiTreeConfig() + self.mip_solver: PersistentSolver = mip_solver + self.nlp_solver: PersistentSolver = nlp_solver + self._original_model: Optional[_BlockData] = None + self._relaxation: Optional[_BlockData] = None + self._nlp: Optional[_BlockData] = None + self._start_time: Optional[float] = None + self._incumbent: Optional[pe.ComponentMap] = None + self._best_feasible_objective: Optional[float] = None + self._best_objective_bound: Optional[float] = None + self._objective: Optional[_GeneralObjectiveData] = None + self._relaxation_objects: Optional[Sequence[BaseRelaxationData]] = None + self._stop: Optional[TerminationCondition] = None + self._discrete_vars: Optional[Sequence[_GeneralVarData]] = None + self._rel_to_nlp_map: Optional[MutableMapping] = None + self._nlp_to_orig_map: Optional[MutableMapping] = None + self._nlp_tightener: Optional[appsi.fbbt.IntervalTightener] = None + self._iter: int = 0 + + def _re_init(self): + self._original_model: Optional[_BlockData] = None + self._relaxation: Optional[_BlockData] = None + self._nlp: Optional[_BlockData] = None + self._start_time: Optional[float] = None + self._incumbent: Optional[pe.ComponentMap] = None + self._best_feasible_objective: Optional[float] = None + self._best_objective_bound: Optional[float] = None + self._objective: Optional[_GeneralObjectiveData] = None + self._relaxation_objects: Optional[Sequence[BaseRelaxationData]] = None + self._stop: Optional[TerminationCondition] = None + self._discrete_vars: Optional[Sequence[_GeneralVarData]] = None + self._rel_to_nlp_map: Optional[MutableMapping] = None + self._nlp_to_orig_map: Optional[MutableMapping] = None + self._nlp_tightener: Optional[appsi.fbbt.IntervalTightener] = None + self._iter: int = 0 + + def available(self): + if ( + self.mip_solver.available() == Solver.Availability.FullLicense + and self.nlp_solver.available() == Solver.Availability.FullLicense + ): + return Solver.Availability.FullLicense + elif self.mip_solver.available() == Solver.Availability.FullLicense: + return self.nlp_solver.available() + else: + return self.mip_solver.available() + + def version(self) -> Tuple: + return 0, 1, 0 + + @property + def config(self) -> MultiTreeConfig: + return self._config + + @config.setter + def config(self, val: MultiTreeConfig): + self._config = val + + @property + def symbol_map(self): + raise NotImplementedError("This solver does not have a symbol map") + + def _should_terminate(self) -> Tuple[bool, Optional[TerminationCondition]]: + if self._elapsed_time >= self.config.time_limit: + return True, TerminationCondition.maxTimeLimit + if self._iter >= self.config.max_iter: + return True, TerminationCondition.maxIterations + if self._stop is not None: + return True, self._stop + primal_bound = self._get_primal_bound() + dual_bound = self._get_dual_bound() + if self._objective.sense == pe.minimize: + assert primal_bound >= dual_bound - 1e-6*max(abs(primal_bound), abs(dual_bound)) - 1e-6 + else: + assert primal_bound <= dual_bound + 1e-6*max(abs(primal_bound), abs(dual_bound)) + 1e-6 + abs_gap, rel_gap = self._get_abs_and_rel_gap() + if abs_gap <= self.config.abs_gap: + return True, TerminationCondition.optimal + if rel_gap <= self.config.mip_gap: + return True, TerminationCondition.optimal + return False, TerminationCondition.unknown + + def _get_results(self, termination_condition: TerminationCondition) -> MultiTreeResults: + res = MultiTreeResults() + res.termination_condition = termination_condition + res.best_feasible_objective = self._best_feasible_objective + res.best_objective_bound = self._best_objective_bound + if self._best_feasible_objective is not None: + res.solution_loader = MultiTreeSolutionLoader(self._incumbent) + res.wallclock_time = self._elapsed_time + + if self.config.load_solution: + if res.best_feasible_objective is not None: + if res.termination_condition != TerminationCondition.optimal: + logger.warning('Loading a feasible but potentially sub-optimal ' + 'solution. Please check the termination condition.') + res.solution_loader.load_vars() + else: + raise RuntimeError('No feasible solution was found. Please ' + 'set opt.config.load_solution=False and check the ' + 'termination condition before loading a solution.') + + return res + + def _get_primal_bound(self) -> float: + if self._best_feasible_objective is None: + if self._objective.sense == pe.minimize: + primal_bound = math.inf + else: + primal_bound = -math.inf + else: + primal_bound = self._best_feasible_objective + return primal_bound + + def _get_dual_bound(self) -> float: + if self._best_objective_bound is None: + if self._objective.sense == pe.minimize: + dual_bound = -math.inf + else: + dual_bound = math.inf + else: + dual_bound = self._best_objective_bound + return dual_bound + + def _get_abs_and_rel_gap(self): + primal_bound = self._get_primal_bound() + dual_bound = self._get_dual_bound() + abs_gap = abs(primal_bound - dual_bound) + if abs_gap == 0: + rel_gap = 0 + elif primal_bound == 0: + rel_gap = math.inf + elif math.isinf(abs_gap): + rel_gap = math.inf + else: + rel_gap = abs_gap / abs(primal_bound) + return abs_gap, rel_gap + + def _get_constr_violation(self): + viol_list = list() + if len(self._relaxation_objects) == 0: + return 0 + for b in self._relaxation_objects: + any_none = False + for v in b.get_rhs_vars(): + if v.value is None: + any_none = True + break + if any_none: + viol_list.append(math.inf) + break + else: + viol_list.append(b.get_deviation()) + return max(viol_list) + + def _log(self, header=False, num_lb_improved=0, num_ub_improved=0, + avg_lb_improvement=0, avg_ub_improvement=0, rel_termination=None, + nlp_termination=None, constr_viol=None): + logger = self.config.solver_output_logger + log_level = self.config.log_level + if header: + msg = ( + f"{'Iter':<6}{'Primal Bd':<12}{'Dual Bd':<12}{'Abs Gap':<9}" + f"{'% Gap':<7}{'CnstrVio':<10}{'Time':<6}{'NLP Term':<10}" + f"{'Rel Term':<10}{'#LBs':<6}{'#UBs':<6}{'Avg LB':<9}" + f"{'Avg UB':<9}" + ) + logger.log(log_level, msg) + if self.config.stream_solver: + print(msg) + else: + if rel_termination is None: + rel_termination = '-' + else: + rel_termination = str(rel_termination.name) + if nlp_termination is None: + nlp_termination = '-' + else: + nlp_termination = str(nlp_termination.name) + rel_termination = rel_termination[:9] + nlp_termination = nlp_termination[:9] + primal_bound = self._get_primal_bound() + dual_bound = self._get_dual_bound() + abs_gap, rel_gap = self._get_abs_and_rel_gap() + if constr_viol is None: + constr_viol = '-' + else: + constr_viol = f'{constr_viol:<10.1e}' + elapsed_time = self._elapsed_time + if elapsed_time < 100: + elapsed_time_str = f'{elapsed_time:<6.2f}' + else: + elapsed_time_str = f'{round(elapsed_time):<6d}' + percent_gap = rel_gap*100 + if math.isinf(percent_gap): + percent_gap_str = f'{percent_gap:<7.2f}' + elif percent_gap >= 100: + percent_gap_str = f'{round(percent_gap):<7d}' + else: + percent_gap_str = f'{percent_gap:<7.2f}' + msg = ( + f"{self._iter:<6}{primal_bound:<12.3e}{dual_bound:<12.3e}" + f"{abs_gap:<9.1e}{percent_gap_str:<7}{constr_viol:<10}" + f"{elapsed_time_str:<6}{nlp_termination:<10}" + f"{rel_termination:<10}{num_lb_improved:<6}" + f"{num_ub_improved:<6}{avg_lb_improvement:<9.1e}" + f"{avg_ub_improvement:<9.1e}" + ) + logger.log(log_level, msg) + if self.config.stream_solver: + print(msg) + + def _update_dual_bound(self, res: Results): + if res.best_objective_bound is not None: + if self._objective.sense == pe.minimize: + if ( + self._best_objective_bound is None + or res.best_objective_bound > self._best_objective_bound + ): + self._best_objective_bound = res.best_objective_bound + else: + if ( + self._best_objective_bound is None + or res.best_objective_bound < self._best_objective_bound + ): + self._best_objective_bound = res.best_objective_bound + + if res.best_feasible_objective is not None: + max_viol = self._get_constr_violation() + if max_viol > self.config.feasibility_tolerance: + all_cons_satisfied = False + else: + all_cons_satisfied = True + if all_cons_satisfied: + for v in self._discrete_vars: + if v.value is None: + assert v.stale + continue + if not math.isclose(v.value, round(v.value), rel_tol=self.config.integer_tolerance, abs_tol=self.config.integer_tolerance): + all_cons_satisfied = False + break + if all_cons_satisfied: + for rel_v, nlp_v in self._rel_to_nlp_map.items(): + if rel_v.value is None: + assert rel_v.stale + if rel_v.has_lb() and rel_v.has_ub() and math.isclose(rel_v.lb, rel_v.ub, rel_tol=self.config.feasibility_tolerance, abs_tol=self.config.feasibility_tolerance): + nlp_v.value = 0.5*(rel_v.lb + rel_v.ub) + else: + nlp_v.value = None + else: + nlp_v.set_value(rel_v.value, skip_validation=True) + self._update_primal_bound(res) + + def _update_primal_bound(self, res: Results): + should_update = False + if res.best_feasible_objective is not None: + if self._objective.sense == pe.minimize: + if ( + self._best_feasible_objective is None + or res.best_feasible_objective < self._best_feasible_objective + ): + should_update = True + else: + if ( + self._best_feasible_objective is None + or res.best_feasible_objective > self._best_feasible_objective + ): + should_update = True + + if should_update: + self._best_feasible_objective = res.best_feasible_objective + self._incumbent = pe.ComponentMap() + for nlp_v, orig_v in self._nlp_to_orig_map.items(): + self._incumbent[orig_v] = nlp_v.value + + def _solve_nlp_with_fixed_vars( + self, + integer_var_values: MutableMapping[_GeneralVarData, float], + rhs_var_bounds: MutableMapping[_GeneralVarData, Tuple[float, float]], + ) -> Results: + self._iter += 1 + + bm = BoundsManager(self._nlp) + bm.save_bounds() + + fixed_vars = list() + for v in self._discrete_vars: + if v.fixed: + continue + val = integer_var_values[v] + assert math.isclose(val, round(val), rel_tol=self.config.integer_tolerance, abs_tol=self.config.integer_tolerance) + val = round(val) + nlp_v = self._rel_to_nlp_map[v] + orig_v = self._nlp_to_orig_map[nlp_v] + nlp_v.fix(val) + orig_v.fix(val) + fixed_vars.append(nlp_v) + fixed_vars.append(orig_v) + + for v, (v_lb, v_ub) in rhs_var_bounds.items(): + if v.fixed: + continue + nlp_v = self._rel_to_nlp_map[v] + nlp_v.setlb(v_lb) + nlp_v.setub(v_ub) + + nlp_res = Results() + + active_constraints = list() + for c in ComponentSet( + self._nlp.component_data_objects( + pe.Constraint, active=True, descend_into=True + ) + ): + active_constraints.append(c) + + try: + self._nlp_tightener.perform_fbbt(self._nlp) + proven_infeasible = False + except InfeasibleConstraintException: + # the original NLP may still be feasible + proven_infeasible = True + + if proven_infeasible: + any_unfixed_vars = False + for v in self._original_model.component_data_objects( + pe.Var, descend_into=True + ): + if not v.fixed: + any_unfixed_vars = True + break + if any_unfixed_vars: + self.nlp_solver.config.time_limit = self._remaining_time + nlp_res = self.nlp_solver.solve(self._original_model) + if nlp_res.best_feasible_objective is not None: + nlp_res.solution_loader.load_vars() + for nlp_v, orig_v in self._nlp_to_orig_map.items(): + nlp_v.set_value(orig_v.value, skip_validation=True) + else: + nlp_res = Results() + nlp_res.termination_condition = TerminationCondition.infeasible + else: + for v in ComponentSet( + self._nlp.component_data_objects(pe.Var, descend_into=True) + ): + if v.fixed: + continue + if v.has_lb() and v.has_ub(): + if math.isclose(v.lb, v.ub, rel_tol=self.config.feasibility_tolerance, abs_tol=self.config.feasibility_tolerance): + v.fix(0.5 * (v.lb + v.ub)) + fixed_vars.append(v) + else: + v.value = 0.5 * (v.lb + v.ub) + + any_unfixed_vars = False + for c in self._nlp.component_data_objects( + pe.Constraint, active=True, descend_into=True + ): + for v in identify_variables(c.body, include_fixed=False): + any_unfixed_vars = True + break + if not any_unfixed_vars: + for obj in self._nlp.component_data_objects( + pe.Objective, active=True, descend_into=True + ): + for v in identify_variables(obj.expr, include_fixed=False): + any_unfixed_vars = True + break + + if any_unfixed_vars: + self.nlp_solver.config.time_limit = self._remaining_time + self.nlp_solver.config.load_solution = False + try: + nlp_res = self.nlp_solver.solve(self._nlp) + solve_error = False + except Exception: + solve_error = True + if not solve_error and nlp_res.best_feasible_objective is not None: + nlp_res.solution_loader.load_vars() + else: + self.nlp_solver.config.time_limit = self._remaining_time + try: + nlp_res = self.nlp_solver.solve(self._original_model) + solve_error = False + except Exception: + solve_error = True + if not solve_error and nlp_res.best_feasible_objective is not None: + nlp_res.solution_loader.load_vars() + for nlp_v, orig_v in self._nlp_to_orig_map.items(): + nlp_v.value = orig_v.value + else: + nlp_obj = get_objective(self._nlp) + # there should not be any active constraints + # they should all have been deactivated by FBBT + for c in active_constraints: + assert not c.active + nlp_res.termination_condition = TerminationCondition.optimal + nlp_res.best_feasible_objective = pe.value(nlp_obj) + nlp_res.best_objective_bound = nlp_res.best_feasible_objective + nlp_res.solution_loader = MultiTreeSolutionLoader(pe.ComponentMap((v, v.value) for v in self._nlp.component_data_objects(pe.Var, descend_into=True))) + + self._update_primal_bound(nlp_res) + self._log(header=False, nlp_termination=nlp_res.termination_condition) + + for v in fixed_vars: + v.unfix() + + bm.pop_bounds() + + for c in active_constraints: + c.activate() + + return nlp_res + + def _solve_relaxation(self) -> Results: + self._iter += 1 + self.mip_solver.config.time_limit = self._remaining_time + self.mip_solver.config.load_solution = False + rel_res = self.mip_solver.solve(self._relaxation) + + if rel_res.best_feasible_objective is not None: + rel_res.solution_loader.load_vars() + + self._update_dual_bound(rel_res) + self._log(header=False, rel_termination=rel_res.termination_condition, constr_viol=self._get_constr_violation()) + if rel_res.termination_condition not in { + TerminationCondition.optimal, + TerminationCondition.maxTimeLimit, + TerminationCondition.maxIterations, + TerminationCondition.objectiveLimit, + TerminationCondition.interrupted, + }: + self._stop = rel_res.termination_condition + return rel_res + + def _partition_helper(self): + dev_list = list() + + err = False + + for b in self._relaxation_objects: + for v in b.get_rhs_vars(): + if not v.has_lb() or not v.has_ub(): + logger.error( + 'The multitree algorithm is not guaranteed to converge ' + 'for problems with unbounded variables. Please bound all ' + 'variables.') + self._stop = TerminationCondition.error + err = True + break + if err: + break + + aux_val = b.get_aux_var().value + rhs_val = pe.value(b.get_rhs_expr()) + if ( + aux_val > rhs_val + self.config.feasibility_tolerance + and b.relaxation_side in {RelaxationSide.BOTH, RelaxationSide.OVER} + and not b.is_rhs_concave() + ): + dev_list.append((b, aux_val - rhs_val)) + elif ( + aux_val < rhs_val - self.config.feasibility_tolerance + and b.relaxation_side in {RelaxationSide.BOTH, RelaxationSide.UNDER} + and not b.is_rhs_convex() + ): + dev_list.append((b, rhs_val - aux_val)) + + if not err: + dev_list.sort(key=lambda x: x[1], reverse=True) + + for b, dev in dev_list[: self.config.max_partitions_per_iter]: + b.add_partition_point() + b.rebuild() + + def _oa_cut_helper(self, tol): + new_con_list = list() + for b in self._relaxation_objects: + new_con = b.add_cut( + keep_cut=True, check_violation=True, feasibility_tol=tol + ) + if new_con is not None: + new_con_list.append(new_con) + self.mip_solver.add_constraints(new_con_list) + return new_con_list + + def _add_oa_cuts(self, tol, max_iter) -> Results: + original_update_config: UpdateConfig = self.mip_solver.update_config() + + self.mip_solver.update() + + self.mip_solver.update_config.update_params = False + self.mip_solver.update_config.update_vars = False + self.mip_solver.update_config.update_objective = False + self.mip_solver.update_config.update_constraints = False + self.mip_solver.update_config.check_for_new_objective = False + self.mip_solver.update_config.check_for_new_or_removed_constraints = False + self.mip_solver.update_config.check_for_new_or_removed_vars = False + self.mip_solver.update_config.check_for_new_or_removed_params = True + self.mip_solver.update_config.treat_fixed_vars_as_params = True + self.mip_solver.update_config.update_named_expressions = False + + last_res = None + + for _iter in range(max_iter): + if self._should_terminate()[0]: + break + + rel_res = self._solve_relaxation() + if rel_res.best_feasible_objective is not None: + last_res = Results() + last_res.best_feasible_objective = rel_res.best_feasible_objective + last_res.best_objective_bound = rel_res.best_objective_bound + last_res.termination_condition = rel_res.termination_condition + last_res.solution_loader = MultiTreeSolutionLoader( + rel_res.solution_loader.get_primals( + vars_to_load=self._discrete_vars + ) + ) + + if self._should_terminate()[0]: + break + + new_con_list = self._oa_cut_helper(tol=tol) + if len(new_con_list) == 0: + break + + self.mip_solver.update_config.update_params = ( + original_update_config.update_params + ) + self.mip_solver.update_config.update_vars = original_update_config.update_vars + self.mip_solver.update_config.update_objective = ( + original_update_config.update_objective + ) + self.mip_solver.update_config.update_constraints = ( + original_update_config.update_constraints + ) + self.mip_solver.update_config.check_for_new_objective = ( + original_update_config.check_for_new_objective + ) + self.mip_solver.update_config.check_for_new_or_removed_constraints = ( + original_update_config.check_for_new_or_removed_constraints + ) + self.mip_solver.update_config.check_for_new_or_removed_vars = ( + original_update_config.check_for_new_or_removed_vars + ) + self.mip_solver.update_config.check_for_new_or_removed_params = ( + original_update_config.check_for_new_or_removed_params + ) + self.mip_solver.update_config.treat_fixed_vars_as_params = ( + original_update_config.treat_fixed_vars_as_params + ) + self.mip_solver.update_config.update_named_expressions = ( + original_update_config.update_named_expressions + ) + + if last_res is None: + last_res = Results() + + return last_res + + def _construct_nlp(self): + all_vars = list( + ComponentSet( + self._original_model.component_data_objects(pe.Var, descend_into=True) + ) + ) + tmp_name = unique_component_name(self._original_model, "all_vars") + setattr(self._original_model, tmp_name, all_vars) + + # this has to be 0 because the Multitree solver cannot use alpha-bb relaxations + max_vars_per_alpha_bb = 0 + max_eigenvalue_for_alpha_bb = 0 + + if self.config.convexity_effort == Effort.none: + perform_expression_simplification = False + use_alpha_bb = False + eigenvalue_bounder = EigenValueBounder.Gershgorin + eigenvalue_opt = None + elif self.config.convexity_effort <= Effort.low: + perform_expression_simplification = False + use_alpha_bb = True + eigenvalue_bounder = EigenValueBounder.Gershgorin + eigenvalue_opt = None + elif self.config.convexity_effort <= Effort.medium: + perform_expression_simplification = True + use_alpha_bb = True + eigenvalue_bounder = EigenValueBounder.GershgorinWithSimplification + eigenvalue_opt = None + elif self.config.convexity_effort <= Effort.high: + perform_expression_simplification = True + use_alpha_bb = True + eigenvalue_bounder = EigenValueBounder.LinearProgram + eigenvalue_opt = self.mip_solver.__class__() + eigenvalue_opt.config = self.mip_solver.config() + # TODO: need to update the solver options + else: + perform_expression_simplification = True + use_alpha_bb = True + eigenvalue_bounder = EigenValueBounder.Global + mip_solver = self.mip_solver.__class__() + mip_solver.config = self.mip_solver.config() + nlp_solver = self.nlp_solver.__class__() + nlp_solver.config = self.nlp_solver.config() + eigenvalue_opt = MultiTree(mip_solver=mip_solver, nlp_solver=nlp_solver) + eigenvalue_opt.config = self.config() + eigenvalue_opt.config.convexity_effort = min(self.config.convexity_effort, Effort.medium) + + self._nlp = relax( + model=self._original_model, + in_place=False, + use_fbbt=True, + fbbt_options={"deactivate_satisfied_constraints": True, "max_iter": 5}, + perform_expression_simplification=perform_expression_simplification, + use_alpha_bb=use_alpha_bb, + eigenvalue_bounder=eigenvalue_bounder, + eigenvalue_opt=eigenvalue_opt, + max_vars_per_alpha_bb=max_vars_per_alpha_bb, + max_eigenvalue_for_alpha_bb=max_eigenvalue_for_alpha_bb, + ) + new_vars = getattr(self._nlp, tmp_name) + self._nlp_to_orig_map = pe.ComponentMap(zip(new_vars, all_vars)) + delattr(self._original_model, tmp_name) + delattr(self._nlp, tmp_name) + + for b in relaxation_data_objects(self._nlp, descend_into=True, active=True): + b.rebuild(build_nonlinear_constraint=True) + + def _construct_relaxation(self): + all_vars = list( + ComponentSet( + self._nlp.component_data_objects(pe.Var, descend_into=True) + ) + ) + tmp_name = unique_component_name(self._nlp, "all_vars") + setattr(self._nlp, tmp_name, all_vars) + self._relaxation = self._nlp.clone() + new_vars = getattr(self._relaxation, tmp_name) + self._rel_to_nlp_map = pe.ComponentMap(zip(new_vars, all_vars)) + delattr(self._nlp, tmp_name) + delattr(self._relaxation, tmp_name) + + for b in relaxation_data_objects(self._relaxation, descend_into=True, active=True): + b.small_coef = self.config.small_coef + b.large_coef = self.config.large_coef + b.safety_tol = self.config.safety_tol + b.rebuild() + + def _get_nlp_specs_from_rel(self): + integer_var_values = pe.ComponentMap() + for v in self._discrete_vars: + integer_var_values[v] = v.value + rhs_var_bounds = pe.ComponentMap() + for r in self._relaxation_objects: + if not isinstance(r, BasePWRelaxationData): + continue + any_unbounded_vars = False + for v in r.get_rhs_vars(): + if not v.has_lb() or not v.has_ub(): + any_unbounded_vars = True + break + if any_unbounded_vars: + continue + active_parts = r.get_active_partitions() + assert len(active_parts) == 1 + v, bnds = list(active_parts.items())[0] + if v in rhs_var_bounds: + existing_bnds = rhs_var_bounds[v] + bnds = (max(bnds[0], existing_bnds[0]), min(bnds[1], existing_bnds[1])) + assert bnds[0] <= bnds[1] + rhs_var_bounds[v] = bnds + return integer_var_values, rhs_var_bounds + + @property + def _elapsed_time(self): + return time.time() - self._start_time + + @property + def _remaining_time(self): + return max(0.0, self.config.time_limit - self._elapsed_time) + + def _perform_obbt(self, vars_to_tighten): + safety_tol = 1e-4 + self._iter += 1 + orig_lbs = list() + orig_ubs = list() + for v in vars_to_tighten: + v_lb, v_ub = v.bounds + if v_lb is None: + v_lb = -math.inf + if v_ub is None: + v_ub = math.inf + orig_lbs.append(v_lb) + orig_ubs.append(v_ub) + orig_lbs = np.array(orig_lbs) + orig_ubs = np.array(orig_ubs) + perform_obbt(self._relaxation, solver=self.mip_solver, + varlist=list(vars_to_tighten), + objective_bound=self._best_feasible_objective, + with_progress_bar=self.config.show_obbt_progress_bar, + time_limit=self._remaining_time) + new_lbs = list() + new_ubs = list() + for ndx, v in enumerate(vars_to_tighten): + v_lb, v_ub = v.bounds + if v_lb is None: + v_lb = -math.inf + if v_ub is None: + v_ub = math.inf + v_lb -= safety_tol + v_ub += safety_tol + if v_lb < orig_lbs[ndx]: + v_lb = orig_lbs[ndx] + if v_ub > orig_ubs[ndx]: + v_ub = orig_ubs[ndx] + v.setlb(v_lb) + v.setub(v_ub) + new_lbs.append(v_lb) + new_ubs.append(v_ub) + for r in self._relaxation_objects: + r.rebuild() + new_lbs = np.array(new_lbs) + new_ubs = np.array(new_ubs) + lb_diff = new_lbs - orig_lbs + ub_diff = orig_ubs - new_ubs + lb_improved = lb_diff > 1e-3 + ub_improved = ub_diff > 1e-3 + lb_improved_indices = lb_improved.nonzero()[0] + ub_improved_indices = ub_improved.nonzero()[0] + num_lb_improved = len(lb_improved_indices) + num_ub_improved = len(ub_improved_indices) + if num_lb_improved > 0: + avg_lb_improvement = np.mean(lb_diff[lb_improved_indices]) + else: + avg_lb_improvement = 0 + if num_ub_improved > 0: + avg_ub_improvement = np.mean(ub_diff[ub_improved_indices]) + else: + avg_ub_improvement = 0 + self._log(header=False, num_lb_improved=num_lb_improved, + num_ub_improved=num_ub_improved, + avg_lb_improvement=avg_lb_improvement, + avg_ub_improvement=avg_ub_improvement) + + return num_lb_improved, num_ub_improved, avg_lb_improvement, avg_ub_improvement + + def solve(self, model: _BlockData, timer: HierarchicalTimer = None) -> MultiTreeResults: + model = clone_active_flat(model) + self._re_init() + + self._start_time = time.time() + if timer is None: + timer = HierarchicalTimer() + timer.start("solve") + + self._original_model = model + + self._log(header=True) + + timer.start("construct relaxation") + self._construct_nlp() + self._construct_relaxation() + timer.stop("construct relaxation") + + self._objective = get_objective(self._relaxation) + self._relaxation_objects = list() + for r in relaxation_data_objects( + self._relaxation, descend_into=True, active=True + ): + self._relaxation_objects.append(r) + + should_terminate, reason = self._should_terminate() + if should_terminate: + return self._get_results(reason) + + self._log(header=False) + + self.mip_solver.set_instance(self._relaxation) + self._nlp_tightener = appsi.fbbt.IntervalTightener() + self._nlp_tightener.config.deactivate_satisfied_constraints = True + self._nlp_tightener.config.feasibility_tol = self.config.feasibility_tolerance + self._nlp_tightener.set_instance(self._nlp, symbolic_solver_labels=False) + + relaxed_binaries, relaxed_integers = push_integers(self._relaxation) + self._discrete_vars = list(relaxed_binaries) + list(relaxed_integers) + oa_results = self._add_oa_cuts(self.config.feasibility_tolerance * 100, 100) + pop_integers(relaxed_binaries, relaxed_integers) + + should_terminate, reason = self._should_terminate() + if should_terminate: + return self._get_results(reason) + + if _is_problem_definitely_convex(self._relaxation): + oa_results = self._add_oa_cuts(self.config.feasibility_tolerance, 100) + else: + oa_results = self._add_oa_cuts(self.config.feasibility_tolerance * 1e3, 3) + + should_terminate, reason = self._should_terminate() + if should_terminate: + return self._get_results(reason) + + if oa_results.best_feasible_objective is not None: + integer_var_values, rhs_var_bounds = self._get_nlp_specs_from_rel() + nlp_res = self._solve_nlp_with_fixed_vars( + integer_var_values, rhs_var_bounds + ) + + vars_to_tighten = collect_vars_to_tighten(self._relaxation) + for obbt_iter in range(self.config.root_obbt_max_iter): + should_terminate, reason = self._should_terminate() + if should_terminate: + return self._get_results(reason) + relaxed_binaries, relaxed_integers = push_integers(self._relaxation) + num_lb, num_ub, avg_lb, avg_ub = self._perform_obbt(vars_to_tighten) + pop_integers(relaxed_binaries, relaxed_integers) + should_terminate, reason = self._should_terminate() + if (num_lb + num_ub) < 1 or (avg_lb < 1e-3 and avg_ub < 1e-3): + break + if should_terminate: + return self._get_results(reason) + self._solve_relaxation() + + while True: + should_terminate, reason = self._should_terminate() + if should_terminate: + break + + rel_res = self._solve_relaxation() + + should_terminate, reason = self._should_terminate() + if should_terminate: + break + + if rel_res.best_feasible_objective is not None: + self._oa_cut_helper(self.config.feasibility_tolerance) + self._partition_helper() + + integer_var_values, rhs_var_bounds = self._get_nlp_specs_from_rel() + start_primal_bound = self._get_primal_bound() + nlp_res = self._solve_nlp_with_fixed_vars( + integer_var_values, rhs_var_bounds + ) + end_primal_bound = self._get_primal_bound() + + should_terminate, reason = self._should_terminate() + if should_terminate: + break + + if self.config.obbt_at_new_incumbents and not math.isclose(start_primal_bound, end_primal_bound, rel_tol=1e-4, abs_tol=1e-4): + if self.config.relax_integers_for_obbt: + relaxed_binaries, relaxed_integers = push_integers(self._relaxation) + num_lb, num_ub, avg_lb, avg_ub = self._perform_obbt(vars_to_tighten) + if self.config.relax_integers_for_obbt: + pop_integers(relaxed_binaries, relaxed_integers) + else: + self.config.solver_output_logger.warning( + f"relaxation did not find a feasible solution: " + f"{rel_res.termination_condition}" + ) + + res = self._get_results(reason) + + timer.stop("solve") + + return res diff --git a/pyomo/contrib/coramin/algorithms/multitree/tests/__init__.py b/pyomo/contrib/coramin/algorithms/multitree/tests/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/pyomo/contrib/coramin/algorithms/multitree/tests/test_multitree.py b/pyomo/contrib/coramin/algorithms/multitree/tests/test_multitree.py new file mode 100644 index 00000000000..dfcb08f8e95 --- /dev/null +++ b/pyomo/contrib/coramin/algorithms/multitree/tests/test_multitree.py @@ -0,0 +1,278 @@ +import math + +import coramin +from coramin.third_party.minlplib_tools import get_minlplib, get_minlplib_instancedata +import unittest +from pyomo.contrib import appsi +import os +import logging +from suspect.pyomo.osil_reader import read_osil +import math +from pyomo.common import download +import pyomo.environ as pe +from pyomo.core.base.block import _BlockData + + +def _get_sol(pname): + current_dir = os.getcwd() + target_fname = os.path.join(current_dir, f'{pname}.sol') + downloader = download.FileDownloader() + downloader.set_destination_filename(target_fname) + downloader.get_binary_file(f'http://www.minlplib.org/sol/{pname}.p1.sol') + res = dict() + f = open(target_fname, 'r') + for line in f.readlines(): + l = line.split() + vname = l[0] + vval = float(l[1]) + if vname != 'objvar': + res[vname] = vval + f.close() + return res + + +class Helper(unittest.TestCase): + def _check_relative_diff(self, expected, got, abs_tol=1e-3, rel_tol=1e-3): + abs_diff = abs(expected - got) + if expected == 0: + rel_diff = math.inf + else: + rel_diff = abs_diff / abs(expected) + success = abs_diff <= abs_tol or rel_diff <= rel_tol + self.assertTrue(success, msg=f'\n expected: {expected}\n got: {got}\n abs diff: {abs_diff}\n rel diff: {rel_diff}') + + +class TestMultiTreeWithMINLPLib(Helper): + @classmethod + def setUpClass(self) -> None: + self.test_problems = {'batch': 285506.5082, + 'ball_mk3_10': None, + 'ball_mk2_10': 0, + 'syn05m': 837.73240090, + 'autocorr_bern20-03': -72, + 'chem': -47.70651483, + 'alkyl': -1.76499965} + self.primal_sol = dict() + self.primal_sol['batch'] = _get_sol('batch') + self.primal_sol['alkyl'] = _get_sol('alkyl') + self.primal_sol['ball_mk2_10'] = _get_sol('ball_mk2_10') + self.primal_sol['syn05m'] = _get_sol('syn05m') + self.primal_sol['autocorr_bern20-03'] = _get_sol('autocorr_bern20-03') + self.primal_sol['chem'] = _get_sol('chem') + for pname in self.test_problems.keys(): + get_minlplib(problem_name=pname) + mip_solver = appsi.solvers.Gurobi() + nlp_solver = appsi.solvers.Ipopt() + nlp_solver.config.log_level = logging.DEBUG + self.opt = coramin.algorithms.MultiTree(mip_solver=mip_solver, + nlp_solver=nlp_solver) + + @classmethod + def tearDownClass(self) -> None: + current_dir = os.getcwd() + for pname in self.test_problems.keys(): + os.remove(os.path.join(current_dir, 'minlplib', 'osil', f'{pname}.osil')) + os.rmdir(os.path.join(current_dir, 'minlplib', 'osil')) + os.rmdir(os.path.join(current_dir, 'minlplib')) + for pname in self.primal_sol.keys(): + os.remove(os.path.join(current_dir, f'{pname}.sol')) + + def get_model(self, pname): + current_dir = os.getcwd() + fname = os.path.join(current_dir, 'minlplib', 'osil', f'{pname}.osil') + m = read_osil(fname, objective_prefix='obj_') + return m + + def _check_primal_sol(self, pname, m: _BlockData, res: appsi.base.Results): + expected_by_str = self.primal_sol[pname] + expected_by_var = pe.ComponentMap() + for vname, vval in expected_by_str.items(): + v = getattr(m, vname) + expected_by_var[v] = vval + got = res.solution_loader.get_primals() + for v, val in expected_by_var.items(): + self._check_relative_diff(val, got[v]) + got = res.solution_loader.get_primals(vars_to_load=list(expected_by_var.keys())) + for v, val in expected_by_var.items(): + self._check_relative_diff(val, got[v]) + + def optimal_helper(self, pname, check_primal_sol=True): + m = self.get_model(pname) + res = self.opt.solve(m) + self.assertEqual(res.termination_condition, appsi.base.TerminationCondition.optimal) + self._check_relative_diff(self.test_problems[pname], res.best_feasible_objective, + abs_tol=self.opt.config.abs_gap, rel_tol=self.opt.config.mip_gap) + self._check_relative_diff(self.test_problems[pname], res.best_objective_bound, + abs_tol=self.opt.config.abs_gap, rel_tol=self.opt.config.mip_gap) + if check_primal_sol: + self._check_primal_sol(pname, m, res) + + def infeasible_helper(self, pname): + m = self.get_model(pname) + self.opt.config.load_solution = False + res = self.opt.solve(m) + self.assertEqual(res.termination_condition, appsi.base.TerminationCondition.infeasible) + self.opt.config.load_solution = True + + def time_limit_helper(self, pname): + orig_time_limit = self.opt.config.time_limit + self.opt.config.load_solution = False + for new_limit in [0, 0.1, 0.2, 0.3]: + self.opt.config.time_limit = new_limit + m = self.get_model(pname) + res = self.opt.solve(m) + self.assertEqual(res.termination_condition, appsi.base.TerminationCondition.maxTimeLimit) + self.opt.config.load_solution = True + + def test_batch(self): + self.optimal_helper('batch') + + def test_ball_mk2_10(self): + self.optimal_helper('ball_mk2_10') + + def test_alkyl(self): + self.optimal_helper('alkyl') + + def test_syn05m(self): + self.optimal_helper('syn05m') + + def test_autocorr_bern20_03(self): + self.optimal_helper('autocorr_bern20-03', check_primal_sol=False) + + def test_chem(self): + orig_config = self.opt.config() + self.opt.config.root_obbt_max_iter = 10 + self.opt.config.mip_gap = 0.05 + self.optimal_helper('chem') + self.opt.config = orig_config + + def test_time_limit(self): + self.time_limit_helper('alkyl') + + def test_ball_mk3_10(self): + self.infeasible_helper('ball_mk3_10') + + def test_available(self): + avail = self.opt.available() + assert avail in appsi.base.Solver.Availability + + +class TestMultiTree(Helper): + def test_convex_overestimator(self): + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(-2, 1)) + m.y = pe.Var() + m.obj = pe.Objective(expr=(m.x + 1)**2 - 0.2*m.y) + m.c = pe.Constraint(expr=m.y <= m.x**2) + mip_solver = appsi.solvers.Gurobi() + nlp_solver = appsi.solvers.Ipopt() + nlp_solver.config.log_level = logging.DEBUG + opt = coramin.algorithms.MultiTree(mip_solver=mip_solver, + nlp_solver=nlp_solver) + res = opt.solve(m) + self.assertEqual(res.termination_condition, appsi.base.TerminationCondition.optimal) + self._check_relative_diff(-0.25, res.best_feasible_objective, + abs_tol=opt.config.abs_gap, rel_tol=opt.config.mip_gap) + self._check_relative_diff(-0.25, res.best_objective_bound, + abs_tol=opt.config.abs_gap, rel_tol=opt.config.mip_gap) + self._check_relative_diff(-1.250953, m.x.value, 1e-2, 1e-2) + self._check_relative_diff(1.5648825, m.y.value, 1e-2, 1e-2) + + def test_max_iter(self): + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(-2, 1)) + m.y = pe.Var() + m.obj = pe.Objective(expr=(m.x + 1)**2 - 0.2*m.y) + m.c = pe.Constraint(expr=m.y <= m.x**2) + mip_solver = appsi.solvers.Gurobi() + nlp_solver = appsi.solvers.Ipopt() + nlp_solver.config.log_level = logging.DEBUG + opt = coramin.algorithms.MultiTree(mip_solver=mip_solver, + nlp_solver=nlp_solver) + opt.config.max_iter = 3 + opt.config.load_solution = False + res = opt.solve(m) + self.assertEqual(res.termination_condition, appsi.base.TerminationCondition.maxIterations) + self.assertIsNone(res.best_feasible_objective) + opt.config.max_iter = 10 + res = opt.solve(m) + self.assertEqual(res.termination_condition, appsi.base.TerminationCondition.maxIterations) + self.assertIsNotNone(res.best_feasible_objective) + + def test_nlp_infeas_fbbt(self): + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(-2, 1), domain=pe.Integers) + m.y = pe.Var(domain=pe.Integers) + m.obj = pe.Objective(expr=(m.x + 1)**2 - 0.2*m.y) + m.c1 = pe.Constraint(expr=m.y <= (m.x - 0.5)**2 - 0.5) + m.c2 = pe.Constraint(expr=m.y >= -(m.x + 2)**2 + 4) + m.c3 = pe.Constraint(expr=m.y <= 2*m.x + 7) + m.c4 = pe.Constraint(expr=m.y >= m.x) + mip_solver = appsi.solvers.Gurobi() + nlp_solver = appsi.solvers.Ipopt() + nlp_solver.config.log_level = logging.DEBUG + opt = coramin.algorithms.MultiTree(mip_solver=mip_solver, + nlp_solver=nlp_solver) + opt.config.load_solution = False + res = opt.solve(m) + self.assertEqual(res.termination_condition, appsi.base.TerminationCondition.infeasible) + + def test_all_vars_fixed_in_nlp(self): + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(-2, 1)) + m.y = pe.Var(domain=pe.Integers) + m.z = pe.Var() + m.obj = pe.Objective(expr=m.z - 0.2*m.y) + m.c1 = pe.Constraint(expr=m.y == (m.x - 0.5)**2 - 0.5) + m.c2 = pe.Constraint(expr=m.z == (m.x + 1)**2) + mip_solver = appsi.solvers.Gurobi() + nlp_solver = appsi.solvers.Ipopt() + nlp_solver.config.log_level = logging.DEBUG + opt = coramin.algorithms.MultiTree(mip_solver=mip_solver, + nlp_solver=nlp_solver) + res = opt.solve(m) + self.assertEqual(res.termination_condition, appsi.base.TerminationCondition.optimal) + self._check_relative_diff(-0.462486082, res.best_feasible_objective) + self._check_relative_diff(-0.462486082, res.best_objective_bound) + self._check_relative_diff(-1.37082869, m.x.value) + self._check_relative_diff(3, m.y.value) + self._check_relative_diff(0.137513918, m.z.value) + + def test_linear_problem(self): + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.obj = pe.Objective(expr=m.y) + m.c1 = pe.Constraint(expr=m.y >= m.x + 1) + m.c2 = pe.Constraint(expr=m.y >= -m.x + 1) + mip_solver = appsi.solvers.Gurobi() + nlp_solver = appsi.solvers.Ipopt() + opt = coramin.algorithms.MultiTree(mip_solver=mip_solver, nlp_solver=nlp_solver) + res = opt.solve(m) + self.assertEqual(res.termination_condition, appsi.base.TerminationCondition.optimal) + self.assertAlmostEqual(res.best_feasible_objective, 1) + self.assertAlmostEqual(res.best_objective_bound, 1) + self.assertAlmostEqual(m.x.value, 0) + self.assertAlmostEqual(m.y.value, 1) + + def test_stale_fixed_vars(self): + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.z = pe.Var(domain=pe.Binary) + m.w = pe.Var() + m.obj = pe.Objective(expr=m.y) + m.c1 = pe.Constraint(expr=m.y >= m.x + 1) + m.c2 = pe.Constraint(expr=m.y >= -m.x + 1) + m.c3 = pe.Constraint(expr=m.w == 2) + mip_solver = appsi.solvers.Gurobi() + nlp_solver = appsi.solvers.Ipopt() + opt = coramin.algorithms.MultiTree(mip_solver=mip_solver, nlp_solver=nlp_solver) + res = opt.solve(m) + self.assertEqual(res.termination_condition, appsi.base.TerminationCondition.optimal) + self.assertAlmostEqual(res.best_feasible_objective, 1) + self.assertAlmostEqual(res.best_objective_bound, 1) + self.assertAlmostEqual(m.x.value, 0) + self.assertAlmostEqual(m.y.value, 1) + self.assertAlmostEqual(m.w.value, 2) + self.assertIsNone(m.z.value) diff --git a/pyomo/contrib/coramin/algorithms/tests/test_ecp_bounder.py b/pyomo/contrib/coramin/algorithms/tests/test_ecp_bounder.py new file mode 100644 index 00000000000..f99dc9f2003 --- /dev/null +++ b/pyomo/contrib/coramin/algorithms/tests/test_ecp_bounder.py @@ -0,0 +1,25 @@ +import pyomo.environ as pe +import coramin +from coramin.algorithms.ecp_bounder import ECPBounder +import unittest +import logging +from pyomo.contrib import appsi + + +logging.basicConfig(level=logging.INFO) + + +class TestECPBounder(unittest.TestCase): + def test_ecp_bounder(self): + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.obj = pe.Objective(expr=0.5*(m.x**2 + m.y**2)) + m.c1 = pe.Constraint(expr=m.y >= (m.x - 1)**2) + m.c2 = pe.Constraint(expr=m.y >= pe.exp(m.x)) + coramin.relaxations.relax(m, in_place=True) + opt = ECPBounder(subproblem_solver=appsi.solvers.Gurobi()) + res = opt.solve(m) + self.assertEqual(res.termination_condition, appsi.base.TerminationCondition.optimal) + self.assertAlmostEqual(m.x.value, 0) + self.assertAlmostEqual(m.y.value, 1) diff --git a/pyomo/contrib/coramin/clone.py b/pyomo/contrib/coramin/clone.py new file mode 100644 index 00000000000..df15b5123d6 --- /dev/null +++ b/pyomo/contrib/coramin/clone.py @@ -0,0 +1,60 @@ +from .relaxations import iterators +from .relaxations.copy_relaxation import copy_relaxation_with_local_data +import pyomo.environ as pe +from .utils.pyomo_utils import get_objective +from pyomo.repn.standard_repn import generate_standard_repn +from pyomo.common.collections import ComponentSet, ComponentMap + + +def clone_active_flat(m1): + m2 = pe.Block(concrete=True) + m2.cons = pe.ConstraintList() + all_vars = ComponentSet() + + # constraints + for c in iterators.nonrelaxation_component_data_objects( + m1, + pe.Constraint, + active=True, + descend_into=True, + ): + lb = pe.value(c.lower) + ub = pe.value(c.upper) + repn = generate_standard_repn(c.body, quadratic=False, compute_values=True) + all_vars.update(repn.linear_vars) + all_vars.update(repn.nonlinear_vars) + body = repn.to_expression() + m2.cons.add((lb, body, ub)) + + # objective + obj = get_objective(m1) + repn = generate_standard_repn(obj.expr, quadratic=False, compute_values=True) + all_vars.update(repn.linear_vars) + all_vars.update(repn.nonlinear_vars) + obj_expr = repn.to_expression() + m2.obj = pe.Objective(expr=obj_expr, sense=obj.sense) + + rel_list = list() + for r in iterators.relaxation_data_objects( + m1, + descend_into=True, + active=True, + ): + rel_list.append(r) + + for ndx, r in enumerate(rel_list): + var_map = ComponentMap() + for v in r.get_rhs_vars(): + if not v.is_fixed(): + all_vars.add(v) + var_map[v] = v + aux_var = r.get_aux_var() + var_map[aux_var] = aux_var + if not aux_var.is_fixed(): + all_vars.add(aux_var) + new_rel = copy_relaxation_with_local_data(r, var_map) + setattr(m2, f'rel{ndx}', new_rel) + + m2.vars = pe.Reference(list(all_vars)) + + return m2 diff --git a/pyomo/contrib/coramin/domain_reduction/__init__.py b/pyomo/contrib/coramin/domain_reduction/__init__.py new file mode 100644 index 00000000000..0668d94bd8e --- /dev/null +++ b/pyomo/contrib/coramin/domain_reduction/__init__.py @@ -0,0 +1,8 @@ +from .obbt import perform_obbt +from .filters import filter_variables_from_solution, aggressive_filter +try: + from .dbt import decompose_model, perform_dbt, perform_dbt_with_integers_relaxed, TreeBlockData, TreeBlock, \ + DecompositionError, TreeBlockError, collect_vars_to_tighten, collect_vars_to_tighten_by_block, DBTInfo, \ + push_integers, pop_integers, OBBTMethod, FilterMethod +except: + pass diff --git a/pyomo/contrib/coramin/domain_reduction/dbt.py b/pyomo/contrib/coramin/domain_reduction/dbt.py new file mode 100644 index 00000000000..c5c9530a525 --- /dev/null +++ b/pyomo/contrib/coramin/domain_reduction/dbt.py @@ -0,0 +1,1542 @@ +import networkx as nx +from typing import Sequence, MutableSet, Optional +from pyomo.contrib.fbbt.fbbt import fbbt, compute_bounds_on_expr +import time +import enum +import warnings +from pyomo.solvers.plugins.solvers.persistent_solver import PersistentSolver +from .obbt import perform_obbt as normal_obbt +from .filters import aggressive_filter +import pyomo.environ as pe +from pyomo.core.expr.visitor import identify_variables +from pyomo.common.collections import ComponentSet +from pyomo.common.collections.orderedset import OrderedSet +from coramin.relaxations.iterators import relaxation_data_objects, nonrelaxation_component_data_objects +from pyomo.core.expr.visitor import replace_expressions +import logging +import networkx +try: + import metis + metis_available = True +except ImportError: + metis_available = False +import numpy as np +import math +from pyomo.core.base.block import declare_custom_block, _BlockData +from coramin.utils.pyomo_utils import get_objective +from pyomo.core.base.var import _GeneralVarData +from coramin.relaxations.copy_relaxation import copy_relaxation_with_local_data +from coramin.relaxations.relaxations_base import BaseRelaxationData +from coramin.utils import RelaxationSide +from collections import defaultdict +from pyomo.core.expr import numeric_expr +from pyomo.core.expr.visitor import ExpressionValueVisitor, nonpyomo_leaf_types +from pyomo.common.modeling import unique_component_name +from coramin.relaxations.split_expr import flatten_expr + + +logger = logging.getLogger(__name__) + + +class DecompositionError(Exception): + pass + + +class TreeBlockError(DecompositionError): + pass + + +@declare_custom_block(name='TreeBlock') +class TreeBlockData(_BlockData): + def __init__(self, component): + _BlockData.__init__(self, component) + self._children_index = None + self._children = None + self._linking_constraints = None + self._already_setup = False + self._is_leaf = None + self._is_root = True + self._allow_changes = False + + def setup(self, children_keys): + assert not self._already_setup + self._already_setup = True + if len(children_keys) == 0: + self._is_leaf = True + else: + self._is_leaf = False + del self._children_index + del self._children + del self._linking_constraints + self._allow_changes = True + self._children_index = pe.Set(initialize=children_keys) + self._children = TreeBlock(self._children_index) + self._linking_constraints = pe.ConstraintList() + self._allow_changes = False + for key in children_keys: + child = self.children[key] + child._is_root = False + + def _assert_setup(self): + if not self._already_setup: + raise TreeBlockError('The TreeBlock has not been setup yet. Please call the setup method.') + + def is_leaf(self): + self._assert_setup() + return self._is_leaf + + def add_component(self, name, val): + self._assert_setup() + if self.is_leaf() or self._allow_changes or not isinstance(val, _GeneralVarData): + _BlockData.add_component(self, name, val) + else: + raise TreeBlockError('Pyomo variables cannot be added to a TreeBlock unless it is a leaf.') + + @property + def children(self): + self._assert_setup() + if self.is_leaf(): + raise TreeBlockError('Leaf TreeBlocks do not have children. Please check the is_leaf method') + return self._children + + @property + def linking_constraints(self): + self._assert_setup() + if self.is_leaf(): + raise TreeBlockError('leaf TreeBlocks do not have linking_constraints. Please check the is_leaf method.') + return self._linking_constraints + + def _num_stages(self): + self._assert_setup() + num_stages = 1 + if not self.is_leaf(): + num_stages += max([child._num_stages() for child in self.children.values()]) + return num_stages + + def num_stages(self): + if not self._is_root: + raise TreeBlockError('The num_stages method can only be called from the root TreeBlock') + return self._num_stages() + + @staticmethod + def _stage_blocks(children, count, stage): + if count == stage: + for child in children.values(): + yield child + else: + for child in children.values(): + if not child.is_leaf(): + for b in TreeBlockData._stage_blocks(child.children, count+1, stage): + yield b + + def stage_blocks(self, stage, active=None): + self._assert_setup() + if not self._is_root: + raise TreeBlockError('The num_stages method can only be called from the root TreeBlock') + if stage == 0: + if (active and self.active) or (not active): + yield self + elif not self.is_leaf(): + for b in self._stage_blocks(self.children, 1, stage): + if (active and b.active) or (not active): + yield b + + def get_block_stage(self, block): + self._assert_setup() + if not self._is_root: + raise TreeBlockError('The get_block_stage method can only be called from the root TreeBlock.') + for stage_ndx in range(self.num_stages()): + stage_blocks = OrderedSet(self.stage_blocks(stage_ndx)) + if block in stage_blocks: + return stage_ndx + return None + + +class _Node(object): + def __init__(self, comp): + self.comp = comp + + def is_var(self): + return False + + def is_con(self): + return False + + def is_rel(self): + return False + + def __repr__(self): + return str(self.comp) + + def __str__(self): + return str(self.comp) + + def __eq__(self, other): + if isinstance(other, _Node): + return self.comp is other.comp + return False + + def __hash__(self): + return hash(id(self.comp)) + + +class _VarNode(_Node): + def is_var(self): + return True + + +class _ConNode(_Node): + def is_con(self): + return True + + +class _RelNode(_Node): + def is_rel(self): + return True + + +class _Edge(object): + def __init__(self, node1: _VarNode, node2: _Node): + assert node1.is_var() + self.node1 = node1 + self.node2 = node2 + + def __str__(self): + s = 'Edge from {0} to {1}'.format(str(self.node1), str(self.node2)) + return s + + +class _Tree(object): + def __init__(self, children=None, edges_between_children=None): + """ + Parameters + ---------- + children: list or collections.abc.Iterable of _Tree or networkx.Graph + edges_between_children: list or collections.abc.Iterable of _Edge + """ + self.children = OrderedSet() + self.edges_between_children = OrderedSet() + if children is not None: + self.children.update(children) + if edges_between_children is not None: + self.edges_between_children.update(edges_between_children) + + def build_pyomo_model(self, block): + """ + Parameters + ---------- + block: TreeBlockData + empty TreeBlock + + Returns + ------- + component_map: pe.ComponentMap + """ + block.setup(children_keys=list(range(len(self.children)))) + component_map = pe.ComponentMap() + replacement_map_by_child = dict() + + for i, child in enumerate(self.children): + if isinstance(child, _Tree): + tmp_component_map = child.build_pyomo_model(block=block.children[i]) + elif isinstance(child, networkx.Graph): + block.children[i].setup(children_keys=list()) + tmp_component_map = build_pyomo_model_from_graph(graph=child, block=block.children[i]) + else: + raise ValueError('Unexpected child type: {0}'.format(str(type(child)))) + replacement_map_by_child[child] = tmp_component_map + component_map.update(tmp_component_map) + + logger.debug('creating linking cons linking the children of {0}'.format(str(block))) + for edge in self.edges_between_children: + logger.debug('adding linking constraint for edge {0}'.format(str(edge))) + if edge.node1.comp is not edge.node2.comp: + raise DecompositionError('Edge {0} node1.comp is not node2.comp'.format(edge)) + if edge.node1.comp not in component_map: + logger.warning('Edge {0} node {1} is not in the component map'.format(str(edge), str(edge.node1))) + all_children = list(self.children) + assert len(all_children) == 2 + child0 = all_children[0] + child1 = all_children[1] + v1 = replacement_map_by_child[child0][edge.node1.comp] + v2 = replacement_map_by_child[child1][edge.node2.comp] + assert v1 is not v2 + block.linking_constraints.add(v1 == v2) + + return component_map + + def log(self, prefix=''): + logger.debug(prefix + '# Edges: {0}'.format(len(self.edges_between_children))) + for _child in self.children: + if isinstance(_child, _Tree): + _child.log(prefix=prefix + ' ') + else: + logger.debug(prefix + ' Leaf: # NNZ: {0}'.format(_child.number_of_edges())) + + +def _is_dominated(ndx, num_cuts, balance, num_cuts_array, balance_array): + cut_diff = ((num_cuts - num_cuts_array) >= 0) + balance_diff = ((abs(balance - 0.5) - abs(balance_array - 0.5)) >= 0) + cut_diff[ndx] = False + balance_diff[ndx] = False + return np.any(cut_diff & balance_diff) + + +def _networkx_to_adjacency_list(graph: networkx.Graph): + adj_list = list() + node_to_ndx_map = dict() + for ndx, node in enumerate(graph.nodes): + node_to_ndx_map[node] = ndx + + for ndx, node in enumerate(graph.nodes): + adj_list.append(list()) + for other_node in graph.adj[node].keys(): + other_ndx = node_to_ndx_map[other_node] + adj_list[ndx].append(other_ndx) + + return adj_list + + +def choose_metis_partition(graph, max_size_diff_trials, seed_trials): + """ + Parameters + ---------- + graph: networkx.Graph + max_size_diff_trials: list of float + seed_trials: list of int + + Returns + ------- + max_size_diff_selected: float + seed_selected: float + """ + if not metis_available: + raise ImportError('Cannot perform graph partitioning without metis. Please install metis (including the python bindings).') + cut_list = list() + for _max_size_diff in max_size_diff_trials: + for _seed in seed_trials: + if _seed is None: + edgecuts, parts = metis.part_graph(_networkx_to_adjacency_list(graph), nparts=2, ubvec=[1 + _max_size_diff]) + else: + edgecuts, parts = metis.part_graph(_networkx_to_adjacency_list(graph), nparts=2, ubvec=[1 + _max_size_diff], seed=_seed) + cut_list.append((edgecuts, sum(parts)/graph.number_of_nodes(), _max_size_diff, _seed)) + cut_list.sort(key=lambda i: i[0]) + + ############################ + # get the "pareto front" obtained with metis + ############################ + num_cuts_array = np.array([i[0] for i in cut_list]) + balance_array = np.array([i[1] for i in cut_list]) + + pareto_list = list() + for ndx, partition in enumerate(cut_list): + num_cuts = partition[0] + balance = partition[1] + if not _is_dominated(ndx, num_cuts, balance, num_cuts_array, balance_array): + pareto_list.append(partition) + if len(pareto_list) == 0: + pareto_list.append(cut_list[0]) + + selection = 0 + chosen_partition = pareto_list[selection] + max_size_diff_selected = chosen_partition[2] + seed_selected = chosen_partition[3] + return max_size_diff_selected, seed_selected + + +def evaluate_partition(original_graph, tree): + """ + Parameters + ---------- + original_graph: networkx.Graph + tree: _Tree + """ + original_graph_nnz = original_graph.number_of_edges() + original_graph_n_vars_to_tighten = len(collect_vars_to_tighten_from_graph(graph=original_graph)) + original_obbt_nnz = original_graph_nnz * original_graph_n_vars_to_tighten + + tree_obbt_nnz = 0 + tree_nnz = 0 + assert len(tree.children) == 2 + for child in tree.children: + assert isinstance(child, networkx.Graph) + child_nnz = child.number_of_edges() + tree_nnz += child_nnz + child_n_vars_to_tighten = len(collect_vars_to_tighten_from_graph(graph=child)) + tree_obbt_nnz += child_nnz * child_n_vars_to_tighten + tree_nnz += 2 * len(tree.edges_between_children) + tree_obbt_nnz += tree_nnz * len(tree.edges_between_children) + partitioning_ratio = original_obbt_nnz / tree_obbt_nnz + return partitioning_ratio + + +def _refine_partition(graph: nx.Graph, model: _BlockData, + removed_edges: Sequence[_Edge], + graph_a_nodes: MutableSet[_Node], + graph_b_nodes: MutableSet[_Node]): + con_count = defaultdict(int) + for edge in removed_edges: + n1, n2 = edge.node1, edge.node2 + if n1.is_con(): + con_count[n1.comp] += 1 + if n2.is_con(): + con_count[n2.comp] += 1 + + for c, count in con_count.items(): + if count < 3: + continue + + new_body = flatten_expr(c.body) + + if type(new_body) is not numeric_expr.SumExpression: + logger.info(f'Constraint {str(c)} is contributing to {count} removed ' + f'edges, but we cannot split the constraint because the ' + f'body is not a SumExpression.') + continue + + graph_a_args = list() + graph_b_args = list() + correct_structure = True + for arg in new_body.args: + graph_a_arg_vars = ComponentSet() + graph_b_arg_vars = ComponentSet() + for v in identify_variables(arg, include_fixed=False): + v_node = _VarNode(v) + assert v_node in graph_a_nodes or v_node in graph_b_nodes + if v_node in graph_a_nodes: + graph_a_arg_vars.add(v) + else: + graph_b_arg_vars.add(v) + if len(graph_a_arg_vars) > 0 and len(graph_b_arg_vars) > 0: + correct_structure = False + break + if len(graph_a_arg_vars) > 0: + graph_a_args.append(arg) + elif len(graph_b_arg_vars) > 0: + graph_b_args.append(arg) + else: + graph_a_args.append(arg) + + if not correct_structure: + logger.info(f'Constriant {str(c)} is contributing to {count} removed ' + f'edges, but we cannot split the constraint because some of ' + f'the terms in the SumExpression contain variables from both ' + f'partitions.') + continue + + # update the model + if not hasattr(model, 'dbt_partition_vars'): + model.dbt_partition_vars = pe.VarList() + model.dbt_partition_cons = pe.ConstraintList() + + graph_a_var = model.dbt_partition_vars.add() + graph_b_var = model.dbt_partition_vars.add() + + if c.lower is not None and c.upper is not None: + new_c1 = model.dbt_partition_cons.add(graph_a_var == sum(graph_a_args)) + new_c2 = model.dbt_partition_cons.add(graph_b_var == sum(graph_b_args)) + if c.equality: + new_c3 = model.dbt_partition_cons.add(graph_a_var + graph_b_var == c.lower) + else: + new_c3 = model.dbt_partition_cons.add((c.lower, graph_a_var + graph_b_var, c.upper)) + elif c.lower is None: + assert c.upper is not None + new_c1 = model.dbt_partition_cons.add(graph_a_var >= sum(graph_a_args)) + new_c2 = model.dbt_partition_cons.add(graph_b_var >= sum(graph_b_args)) + new_c3 = model.dbt_partition_cons.add(graph_a_var + graph_b_var <= c.upper) + else: + assert c.upper is None + new_c1 = model.dbt_partition_cons.add(graph_a_var <= sum(graph_a_args)) + new_c2 = model.dbt_partition_cons.add(graph_b_var <= sum(graph_b_args)) + new_c3 = model.dbt_partition_cons.add(graph_a_var + graph_b_var >= c.lower) + c.deactivate() + + # update the graph + graph.remove_node(_ConNode(c)) + graph.add_node(_VarNode(graph_a_var)) + graph.add_node(_VarNode(graph_b_var)) + for new_con in [new_c1, new_c2, new_c3]: + graph.add_node(_ConNode(new_con)) + for v in identify_variables(new_con.body, include_fixed=False): + graph.add_edge(_VarNode(v), _ConNode(new_con)) + + # update removed_edges + new_removed_edges = list() + for e in removed_edges: + if e.node2.comp is not c: + new_removed_edges.append(e) + + new_removed_edges.append(_Edge(_VarNode(graph_a_var), _ConNode(new_c3))) + removed_edges = new_removed_edges + + # update graph_a_nodes and graph_b_nodes + graph_a_nodes.discard(_ConNode(c)) + graph_b_nodes.discard(_ConNode(c)) + graph_a_nodes.add(_VarNode(graph_a_var)) + graph_b_nodes.add(_VarNode(graph_b_var)) + graph_a_nodes.add(_ConNode(new_c1)) + graph_b_nodes.add(_ConNode(new_c2)) + graph_b_nodes.add(_ConNode(new_c3)) + + return removed_edges + + +def split_metis(graph, model): + """ + Parameters + ---------- + graph: networkx.Graph + model: _BlockData + + Returns + ------- + tree: _Tree + """ + if not metis_available: + raise ImportError('Cannot perform graph partitioning without metis. Please install metis (including the python bindings).') + max_size_diff, seed = choose_metis_partition(graph, max_size_diff_trials=[0.15], seed_trials=list(range(10))) + if seed is None: + edgecuts, parts = metis.part_graph(_networkx_to_adjacency_list(graph), nparts=2, ubvec=[1 + max_size_diff]) + else: + edgecuts, parts = metis.part_graph(_networkx_to_adjacency_list(graph), nparts=2, ubvec=[1 + max_size_diff], seed=seed) + + graph_a_nodes = OrderedSet() + graph_b_nodes = OrderedSet() + for ndx, n in enumerate(graph.nodes()): + if parts[ndx] == 0: + graph_a_nodes.add(n) + else: + assert parts[ndx] == 1 + graph_b_nodes.add(n) + + removed_edges = list() + for n1, n2 in graph.edges(): + if not n1.is_var(): + assert n2.is_var() + n1, n2 = n2, n1 + else: + assert not n2.is_var() + if n1 in graph_a_nodes and n2 in graph_a_nodes: + continue + elif n1 in graph_b_nodes and n2 in graph_b_nodes: + continue + else: + removed_edges.append(_Edge(n1, n2)) + + removed_edges = _refine_partition(graph=graph, model=model, + removed_edges=removed_edges, + graph_a_nodes=graph_a_nodes, + graph_b_nodes=graph_b_nodes) + + graph_a_edges = list() + graph_b_edges = list() + for n1, n2 in graph.edges(): + if not n1.is_var(): + assert n2.is_var() + n1, n2 = n2, n1 + else: + assert not n2.is_var() + if n1 in graph_a_nodes and n2 in graph_a_nodes: + graph_a_edges.append((n1, n2)) + elif n1 in graph_b_nodes and n2 in graph_b_nodes: + graph_b_edges.append((n1, n2)) + else: + continue + + linking_edges = list() + new_var_nodes_dict = dict() + for e in removed_edges: + n1, n2 = e.node1, e.node2 + assert n1.is_var() + assert not n2.is_var() + if n1 in new_var_nodes_dict: + new_var_node = new_var_nodes_dict[n1] + else: + new_var_node = _VarNode(n1.comp) + new_var_nodes_dict[n1] = new_var_node + linking_edge = _Edge(n1, new_var_node) + linking_edges.append(linking_edge) + if n1 in graph_a_nodes: + assert n2 in graph_b_nodes + graph_b_edges.append((new_var_node, n2)) + else: + assert n1 in graph_b_nodes + assert n2 in graph_a_nodes + graph_a_edges.append((new_var_node, n2)) + + graph_a = networkx.Graph() + graph_b = networkx.Graph() + + graph_a.add_nodes_from(graph_a_nodes) + graph_b.add_nodes_from(graph_b_nodes) + graph_a.add_edges_from(graph_a_edges) + graph_b.add_edges_from(graph_b_edges) + + if ((graph_a.number_of_nodes() >= 0.99 * graph.number_of_nodes()) or + (graph_b.number_of_nodes() >= 0.99 * graph.number_of_nodes())): + raise DecompositionError('Failed to partition graph') + + tree = _Tree(children=[graph_a, graph_b], edges_between_children=linking_edges) + + partitioning_ratio = evaluate_partition(original_graph=graph, tree=tree) + + return tree, partitioning_ratio + + +def convert_pyomo_model_to_bipartite_graph(m: _BlockData): + """ + Parameters + ---------- + m: _BlockData + + Returns + ------- + graph: networkx.Graph + """ + graph = networkx.Graph() + var_map = pe.ComponentMap() + + for v in nonrelaxation_component_data_objects(m, pe.Var, sort=True, descend_into=True): + if v.fixed: + continue + var_map[v] = _VarNode(v) + graph.add_node(var_map[v]) + + for b in relaxation_data_objects(m, descend_into=True, active=True, sort=True): + node2 = _RelNode(b) + for v in (list(b.get_rhs_vars()) + [b.get_aux_var()]): + node1 = var_map[v] + graph.add_edge(node1, node2) + + for c in nonrelaxation_component_data_objects(m, pe.Constraint, active=True, sort=True, descend_into=True): + node2 = _ConNode(c) + for v in identify_variables(c.body, include_fixed=False): + node1 = var_map[v] + graph.add_edge(node1, node2) + + return graph + + +def build_pyomo_model_from_graph(graph, block): + """ + Parameters + ---------- + graph: networkx.Graph + block: pe.Block + + Returns + ------- + component_map: pe.ComponentMap + """ + vars = list() + cons = list() + rels = list() + var_names = list() + con_names = list() + rel_names = list() + for node in graph.nodes(): + if node.is_var(): + vars.append(node) + var_names.append(node.comp.getname(fully_qualified=True).replace('.', '_')) + elif node.is_con(): + cons.append(node) + con_names.append(node.comp.getname(fully_qualified=True).replace('.', '_')) + else: + assert node.is_rel() + rels.append(node) + rel_names.append(node.comp.getname(fully_qualified=True).replace('.', '_')) + + assert len(vars) == len(set(vars)) + assert len(cons) == len(set(cons)) + assert len(rels) == len(set(rels)) + + block.var_names = pe.Set(initialize=var_names) + block.con_names = pe.Set(initialize=con_names) + block.vars = pe.Var(block.var_names) + block.cons = pe.Constraint(block.con_names) + block.rels = pe.Block() + + component_map = pe.ComponentMap() + for v_name, v in zip(var_names, vars): + new_v = block.vars[v_name] + component_map[v.comp] = new_v + new_v.setlb(v.comp.lb) + new_v.setub(v.comp.ub) + new_v.domain = v.comp.domain + if v.comp.is_fixed(): + new_v.fix(v.comp.value) + new_v.set_value(v.comp.value, skip_validation=True) + + var_map = {id(k): v for k, v in component_map.items()} + + for c_name, c in zip(con_names, cons): + if c.comp.equality: + block.cons[c_name] = (replace_expressions(c.comp.body, substitution_map=var_map, + remove_named_expressions=True) == c.comp.lower) + else: + block.cons[c_name] = (pe.inequality(lower=c.comp.lower, + body=replace_expressions(c.comp.body, substitution_map=var_map, + remove_named_expressions=True), + upper=c.comp.upper)) + component_map[c.comp] = block.cons[c_name] + + for r_name, r in zip(rel_names, rels): + new_rel = copy_relaxation_with_local_data(r.comp, var_map) + setattr(block.rels, r_name, new_rel) + new_rel.rebuild() + component_map[r.comp] = new_rel + + return component_map + + +def num_cons_in_graph(graph, include_rels=True): + res = 0 + + if include_rels: + for n in graph.nodes(): + if n.is_con() or n.is_rel(): + res += 1 + else: + for n in graph.nodes(): + if n.is_con(): + res += 1 + + return res + + +class DecompositionStatus(enum.Enum): + normal = 0 # the model was successfullay decomposed at least once and no exception was raised + error = 1 # an exception was raised + bad_ratio = 2 # the model could not be decomposed at all because the min_parition_ratio was not satisfied + problem_too_small = 3 # the model could not be decomposed at all because the number of jacobian nonzeros in the original problem was less than max_leaf_nnz + + +def compute_partition_ratio(original_model: _BlockData, decomposed_model: TreeBlockData): + graph = convert_pyomo_model_to_bipartite_graph(original_model) + pr_numerator = graph.number_of_edges() * len(collect_vars_to_tighten(original_model)) + + pr_denominator = 0 + vars_to_tighten_by_block = collect_vars_to_tighten_by_block(decomposed_model, 'dbt') + for block, vars_to_tighten in vars_to_tighten_by_block.items(): + pr_denominator += len(vars_to_tighten) * convert_pyomo_model_to_bipartite_graph(block).number_of_edges() + pr = pr_numerator / pr_denominator + return pr + + +def _reformulate_objective(model): + current_obj = get_objective(model) + if current_obj is None: + raise ValueError('No active objective found!') + if not current_obj.expr.is_variable_type(): + obj_var_name = unique_component_name(model, 'obj_var') + obj_var = pe.Var(bounds=compute_bounds_on_expr(current_obj.expr)) + model.add_component(obj_var_name, obj_var) + model.del_component(current_obj) + new_objective = pe.Objective(expr=model.obj_var) + new_obj_name = unique_component_name(model, 'objective') + model.add_component(new_obj_name, new_objective) + if current_obj.sense == pe.minimize: + obj_con = pe.Constraint(expr=current_obj.expr <= obj_var) + else: + obj_con = pe.Constraint(expr=current_obj.expr >= obj_var) + new_objective.sense = pe.maximize + obj_con_name = unique_component_name(model, 'obj_con') + model.add_component(obj_con_name, obj_con) + + +def _eliminate_mutable_params(model): + sub_map = dict() + for p in nonrelaxation_component_data_objects(model, pe.Param, descend_into=True): + sub_map[id(p)] = p.value + + for c in nonrelaxation_component_data_objects(model, pe.Constraint, active=True, descend_into=True): + if c.lower is None: + new_lower = None + else: + new_lower = replace_expressions(c.lower, sub_map, + descend_into_named_expressions=True, + remove_named_expressions=True) + new_body = replace_expressions(c.body, sub_map, + descend_into_named_expressions=True, + remove_named_expressions=True) + if c.upper is None: + new_upper = None + else: + new_upper = replace_expressions(c.upper, sub_map, + descend_into_named_expressions=True, + remove_named_expressions=True) + c.set_value((new_lower, new_body, new_upper)) + + +def _decompose_model(model: _BlockData, max_leaf_nnz: Optional[int] = None, + min_partition_ratio: float = 1.25, limit_num_stages: bool = True): + """ + Parameters + ---------- + model: _BlockData + The model to decompose + max_leaf_nnz: int + maximum number nonzeros in the constraint jacobian of the leaves + min_partition_ratio: float + If the partition ration is less than min_partition_ratio, the partition is not + accepted and partitioning stops. This value should be between 1 and 2. + limit_num_stages: bool + If True, partitioning will stop before the number of stages produced exceeds + round(math.log10(number of nonzeros in the constraint jacobian of model)) + + Returns + ------- + new_model: TreeBlockData + The decomposed model + component_map: pe.ComponentMap + A ComponentMap mapping varialbes and constraints in model to those in new_model + termination_reason: DecompositionStatus + An enum member from DecompositionStatus + """ + + # by reformulating the objective, we can make better use of the incumbent when + # doing OBBT + _reformulate_objective(model) + # we don't want the original param objects to be in the new model + _eliminate_mutable_params(model) + + graph = convert_pyomo_model_to_bipartite_graph(model) + logger.debug('converted pyomo model to bipartite graph') + original_nnz = graph.number_of_edges() + if limit_num_stages: + max_stages = round(math.log10(original_nnz)) + else: + max_stages = math.inf + logger.debug('NNZ in original graph: {0}'.format(original_nnz)) + logger.debug('maximum number of stages: {0}'.format(max_stages)) + if max_leaf_nnz is None: + max_leaf_nnz = 0.1 * original_nnz + + if original_nnz <= max_leaf_nnz or num_cons_in_graph(graph) <= 1: + if original_nnz <= max_leaf_nnz: + logger.debug('too few NNZ in original graph; not decomposing') + else: + logger.debug('Cannot decompose graph with less than 2 constraints.') + new_model = TreeBlock(concrete=True) + new_model.setup(children_keys=list()) + component_map = build_pyomo_model_from_graph(graph=graph, block=new_model) + termination_reason = DecompositionStatus.problem_too_small + logger.debug('done building pyomo model from graph') + else: + root_tree, partitioning_ratio = split_metis(graph=graph, model=model) + logger.debug('partitioned original tree; partitioning ratio: {ratio}'.format( + ratio=partitioning_ratio)) + if partitioning_ratio < min_partition_ratio: + logger.debug('obtained bad partitioning ratio; abandoning partition') + new_model = TreeBlock(concrete=True) + new_model.setup(children_keys=list()) + component_map = build_pyomo_model_from_graph(graph=graph, block=new_model) + termination_reason = DecompositionStatus.bad_ratio + logger.debug('done building pyomo model from graph') + else: + parent = root_tree + + termination_reason = DecompositionStatus.normal + needs_split = list() + for child in parent.children: + logger.debug( + 'number of NNZ in child: {0}'.format(child.number_of_edges())) + if child.number_of_edges() > max_leaf_nnz and num_cons_in_graph( + child) > 1: + needs_split.append((child, parent, 1)) + + while len(needs_split) > 0: + logger.debug('needs_split: {0}'.format(str(needs_split))) + _graph, _parent, _stage = needs_split.pop() + try: + if _stage + 1 >= max_stages: + logger.debug(f'stage {_stage}: not partitiong graph with ' + f'{_graph.number_of_edges()} NNZ due to the max ' + f'stages rule;') + continue + logger.debug(f'stage {_stage}: partitioning graph with ' + f'{_graph.number_of_edges()} NNZ') + sub_tree, partitioning_ratio = split_metis(graph=_graph, + model=model) + logger.debug( + 'partitioning ratio: {ratio}'.format(ratio=partitioning_ratio)) + if partitioning_ratio > min_partition_ratio: + logger.debug('partitioned {0}'.format(str(_graph))) + _parent.children.discard(_graph) + _parent.children.add(sub_tree) + + for child in sub_tree.children: + logger.debug('number of NNZ in child: {0}'.format( + child.number_of_edges())) + if (child.number_of_edges() > max_leaf_nnz + and num_cons_in_graph(child) > 1): + needs_split.append((child, sub_tree, _stage + 1)) + else: + logger.debug( + 'obtained bad partitioning ratio; abandoning partition') + except DecompositionError: + termination_reason = DecompositionStatus.error + logger.error('failed to partition graph with {0} NNZ'.format( + _graph.number_of_edges())) + + logger.debug('Tree Info:') + root_tree.log() + + new_model = TreeBlock(concrete=True) + component_map = root_tree.build_pyomo_model(block=new_model) + logger.debug('done building pyomo model from tree') + + obj = get_objective(model) + if obj is not None: + var_map = {id(k): v for k, v in component_map.items()} + new_model.objective = pe.Objective( + expr=replace_expressions(obj.expr, substitution_map=var_map, + remove_named_expressions=True), + sense=obj.sense) + logger.debug('done adding objective to new model') + else: + logger.debug('No objective was found to add to the new model') + + return new_model, component_map, termination_reason + + +def decompose_model(model: _BlockData, max_leaf_nnz: Optional[int] = None, + min_partition_ratio: float = 1.25, limit_num_stages: bool = True): + """ + Parameters + ---------- + model: _BlockData + The model to decompose + max_leaf_nnz: int + maximum number nonzeros in the constraint jacobian of the leaves + min_partition_ratio: float + If the partition ration is less than min_partition_ratio, the partition is not + accepted and partitioning stops. This value should be between 1 and 2. + limit_num_stages: bool + If True, partitioning will stop before the number of stages produced exceeds + round(math.log10(number of nonzeros in the constraint jacobian of model)) + + Returns + ------- + new_model: TreeBlockData + The decomposed model + component_map: pe.ComponentMap + A ComponentMap mapping varialbes and constraints in model to those in new_model + termination_reason: DecompositionStatus + An enum member from DecompositionStatus + """ + # we have to clone the model because we modify it in _refine_partition + all_comps = list(ComponentSet( + nonrelaxation_component_data_objects(model, pe.Var, descend_into=True))) + all_comps.extend(ComponentSet( + nonrelaxation_component_data_objects(model, pe.Constraint, active=True, + descend_into=True))) + all_comps.extend(relaxation_data_objects(model, descend_into=True, active=True)) + all_comps.extend(ComponentSet( + nonrelaxation_component_data_objects(model, pe.Objective, active=True, + descend_into=True))) + tmp_name = unique_component_name(model, 'all_comps') + setattr(model, tmp_name, all_comps) + new_model = model.clone() + old_to_new_comps_map = pe.ComponentMap(zip(getattr(model, tmp_name), + getattr(new_model, tmp_name))) + delattr(model, tmp_name) + delattr(new_model, tmp_name) + model = new_model + + tmp = _decompose_model(model, max_leaf_nnz=max_leaf_nnz, + min_partition_ratio=min_partition_ratio, + limit_num_stages=limit_num_stages) + tree_model, component_map, termination_reason = tmp + + for orig_comp, clone_comp in list(old_to_new_comps_map.items()): + if clone_comp in component_map: + old_to_new_comps_map[orig_comp] = component_map[clone_comp] + + return tree_model, old_to_new_comps_map, termination_reason + + +def collect_vars_to_tighten_from_graph(graph): + vars_to_tighten = ComponentSet() + + for n in graph.nodes(): + if n.is_rel(): + rel: BaseRelaxationData = n.comp + if rel.is_rhs_convex() and rel.relaxation_side == RelaxationSide.UNDER and not rel.use_linear_relaxation: + continue + if rel.is_rhs_concave() and rel.relaxation_side == RelaxationSide.OVER and not rel.use_linear_relaxation: + continue + vars_to_tighten.update(rel.get_rhs_vars()) + elif n.is_var(): + v = n.comp + if v.is_binary() or v.is_integer(): + vars_to_tighten.add(v) + + return vars_to_tighten + + +def collect_vars_to_tighten(block): + graph = convert_pyomo_model_to_bipartite_graph(block) + vars_to_tighten = collect_vars_to_tighten_from_graph(graph=graph) + return vars_to_tighten + + +def collect_vars_to_tighten_by_block(m, method): + """ + Parameters + ---------- + m: TreeBlockData + method: str + 'full_space', 'dbt', or 'leaves' + + Returns + ------- + vars_to_tighten_by_block: dict + maps Block to ComponentSet of Var + """ + assert method in {'full_space', 'dbt', 'leaves'} + + vars_to_tighten_by_block = dict() + + assert isinstance(m, TreeBlockData) + + all_vars_to_account_for = collect_vars_to_tighten(m) + + for stage in range(m.num_stages()): + for block in m.stage_blocks(stage, active=True): + if block.is_leaf(): + vars_to_tighten_by_block[block] = collect_vars_to_tighten(block=block) + elif method == 'leaves': + vars_to_tighten_by_block[block] = ComponentSet() + elif method == 'full_space': + vars_to_tighten_by_block[block] = ComponentSet() + else: + vars_to_tighten_by_block[block] = ComponentSet() + for c in block.linking_constraints.values(): + if c.active: + vars_in_con = list(identify_variables(c.body)) + vars_to_tighten_by_block[block].add(vars_in_con[0]) + + for block, vars_to_tighten in vars_to_tighten_by_block.items(): + for v in vars_to_tighten: + all_vars_to_account_for.discard(v) + + if len(all_vars_to_account_for) != 0: + raise RuntimeError('There are variables that need tightened that are unaccounted for!') + + return vars_to_tighten_by_block + + +class OBBTMethod(enum.Enum): + FULL_SPACE = 1 + DECOMPOSED = 2 + LEAVES = 3 + + +class FilterMethod(enum.Enum): + NONE = 1 + AGGRESSIVE = 2 + + +class DBTInfo(object): + """ + Attributes + ---------- + num_coupling_vars_to_tighten: int + The total number of coupling variables that need tightened. Note that this includes + coupling variables that get filtered. If you subtract num_coupling_vars_attempted + and num_coupling_vars_filtered from num_coupling_vars_to_tighten, you should get + the number of coupling variables that were not tightened due to a time limit. + num_coupling_vars_attempted: int + The number of coupling variables for which tightening was attempted. + num_coupling_vars_successful: int + The number of coupling variables for which tightening was attempted and the solver + terminated optimally. + num_coupling_vars_filtered: int + The number of coupling vars that did not need to be tightened (identified by filtering). + num_vars_to_tighten: int + The total number of nonlinear and discrete variables that need tightened. Note that + this includes variables that get filtered. If you subtract num_vars_attempted and + num_vars_filtered from num_vars_to_tighten, you should get the number of nonlinear + and discrete variables that were not tightened due to a time limit. + num_vars_attempted: int + The number of variables for which tightening was attempted. + num_vars_successful: int + The number of variables for which tightening was attempted and the solver + terminated optimally. + num_vars_filtered: int + The number of vars that did not need to be tightened (identified by filtering). + """ + def __init__(self): + self.num_coupling_vars_to_tighten = None + self.num_coupling_vars_attempted = None + self.num_coupling_vars_successful = None + self.num_coupling_vars_filtered = None + self.num_vars_to_tighten = None + self.num_vars_attempted = None + self.num_vars_successful = None + self.num_vars_filtered = None + + def __str__(self): + s = f'num_coupling_vars_to_tighten: {self.num_coupling_vars_to_tighten}\n' + s += f'num_coupling_vars_attempted: {self.num_coupling_vars_attempted}\n' + s += f'num_coupling_vars_successful: {self.num_coupling_vars_successful}\n' + s += f'num_coupling_vars_filtered: {self.num_coupling_vars_filtered}\n' + s += f'num_vars_to_tighten: {self.num_vars_to_tighten}\n' + s += f'num_vars_attempted: {self.num_vars_attempted}\n' + s += f'num_vars_successful: {self.num_vars_successful}\n' + s += f'num_vars_filtered: {self.num_vars_filtered}\n' + return s + + +def _update_var_bounds(varlist, new_lower_bounds, new_upper_bounds, feasibility_tol, safety_tol, max_acceptable_bound): + for ndx, v in enumerate(varlist): + new_lb = new_lower_bounds[ndx] + new_ub = new_upper_bounds[ndx] + orig_lb = v.lb + orig_ub = v.ub + + if new_lb is None: + new_lb = -math.inf + if new_ub is None: + new_ub = math.inf + if orig_lb is None: + orig_lb = -math.inf + if orig_ub is None: + orig_ub = math.inf + + rel_lb_safety = safety_tol * abs(new_lb) + rel_ub_safety = safety_tol * abs(new_ub) + new_lb -= max(safety_tol, rel_lb_safety) + new_ub += max(safety_tol, rel_ub_safety) + + if new_lb < -max_acceptable_bound: + new_lb = -math.inf + if new_ub > max_acceptable_bound: + new_ub = math.inf + + if new_lb > new_ub: + msg = 'variable ub is less than lb; var: {0}; lb: {1}; ub: {2}'.format(str(v), new_lb, new_ub) + if new_lb > new_ub + feasibility_tol: + raise ValueError(msg) + else: + logger.warning(msg + '; decreasing lb and increasing ub by {0}'.format(feasibility_tol)) + warnings.warn(msg) + new_lb -= feasibility_tol + new_ub += feasibility_tol + + if new_lb < orig_lb: + new_lb = orig_lb + if new_ub > orig_ub: + new_ub = orig_ub + + if new_lb > -math.inf: + v.setlb(new_lb) + if new_ub < math.inf: + v.setub(new_ub) + + +def perform_dbt(relaxation, solver, obbt_method=OBBTMethod.DECOMPOSED, + filter_method=FilterMethod.AGGRESSIVE, time_limit=math.inf, + objective_bound=None, with_progress_bar=False, parallel=False, + vars_to_tighten_by_block=None, feasibility_tol=0, + safety_tol=0, max_acceptable_bound=math.inf, update_relaxations_between_stages=True): + """This function performs optimization-based bounds tightening (OBBT) with a decomposition scheme. + + Parameters + ---------- + relaxation: dbt.decomp.decompose.TreeBlockData + The relaxation to use for OBBT. + solver: pyomo solver object + The solver to use for the OBBT problems. + obbt_method: OBBTMethod + An enum member from OBBTMethod. The default is OBBTMethod.DECOMPOSED. If obbt_method + is OBBTMethod.DECOMPOSED, then only the coupling variables in the linking constraints + will be tightened with non-leaf blocks in relaxation. The nonlinear and discrete + variables will only be tightened with the leaf blocks in relaxation. See the + documentation on TreeBlockData for more details on leaf blocks. If the method is + OBBTMethod.FULL_SPACE, then all of the nonlinear and discrete variables will + be tightened with the root block from relaxation. If the method is + OBBTMethod.LEAVES, then the nonlinear and discrete variables will be tightened + with the leaf blocks from relaxation (none of the coupling variables will be + tightened). + filter_method: FilterMethod + An enum member from FilterMethod. The default is FilterMethod.AGGRESSIVE. If + filter_method is FilterMethod.AGGRESSIVE, then aggressive filtering will be + performed at every stage of OBBT using the + coramin.domain_reduction.filters.aggressive_filter function which is based on + + Gleixner, Ambros M., et al. "Three enhancements for + optimization-based bound tightening." Journal of Global + Optimization 67.4 (2017): 731-757. + + If filter_method is FilterMethod.NONE, then no filtering will be performed. + time_limit: float + If the time spent in this function exceeds time_limit, OBBT will be terminated + early. + objective_bound: float + A lower or upper bound on the objective. If this is not None, then a constraint will be added to the + bounds tightening problems constraining the objective to be less than/greater than objective_bound. + with_progress_bar: bool + parallel: bool + If True, then OBBT will automatically be performed in parallel if mpirun or mpiexec was used; + If False, then OBBT will not run in parallel even if mpirun or mpiexec was used; + vars_to_tighten_by_block: dict + Dictionary mapping TreeBlockData to ComponentSet. This dictionary indicates which variables + should be tightened with which parts of the TreeBlockData. If None is passed (default=None), + then, the function collect_vars_to_tighten_by_block is used to get the dict. + feasibility_tol: float + If the lower bound for a computed variable is larger than the computed upper bound by more than + feasibility_tol, then an error is raised. If the computed lower bound is larger than the computed + upper bound, but by less than feasibility_tol, then the computed lower bound is decreased by + feasibility tol (but will not be set lower than the original lower bound) and the computed upper + bound is increased by feasibility_tol (but will not be set higher than the original upper bound). + safety_tol: float + Computed lower bounds will be decreased by max(safety_tol, safety_tol*abs(new_lb) and + computed upper bounds will be increased by max(safety_tol, safety_tol*abs(new_ub) where + new_lb and new_ub are the bounds computed from OBBT/DBT. The purpose of this is to + account for numerical error in the solution of the OBBT problems and to avoid cutting + off valid portions of the feasible region. + max_acceptable_bound: float + If the upper bound computed for a variable is larger than max_acceptable_bound, then the + computed bound will be rejected. If the lower bound computed for a variable is less than + -max_acceptable_bound, then the computed bound will be rejected. + update_relaxations_between_stages: bool + This is meant for unit testing only and should not be modified + + Returns + ------- + dbt_info: DBTInfo + + """ + t0 = time.time() + + if not isinstance(relaxation, TreeBlockData): + raise ValueError('relaxation must be an instance of dbt.decomp.TreeBlockData.') + if obbt_method not in OBBTMethod: + raise ValueError('obbt_method must be a member of OBBTMethod.') + if filter_method not in FilterMethod: + raise ValueError('filter_method must a member of FilterMethod.') + if isinstance(solver, PersistentSolver): + using_persistent_solver = True + else: + using_persistent_solver = False + + dbt_info = DBTInfo() + dbt_info.num_coupling_vars_to_tighten = 0 + dbt_info.num_coupling_vars_attempted = 0 + dbt_info.num_coupling_vars_successful = 0 + dbt_info.num_coupling_vars_filtered = 0 + dbt_info.num_vars_to_tighten = 0 + dbt_info.num_vars_attempted = 0 + dbt_info.num_vars_successful = 0 + dbt_info.num_vars_filtered = 0 + + assert obbt_method in OBBTMethod + if vars_to_tighten_by_block is None: + if obbt_method == OBBTMethod.DECOMPOSED: + _method = 'dbt' + elif obbt_method == OBBTMethod.FULL_SPACE: + _method = 'full_space' + else: + _method = 'leaves' + vars_to_tighten_by_block = collect_vars_to_tighten_by_block(relaxation, _method) + + var_to_relaxation_map = pe.ComponentMap() + for r in relaxation_data_objects(relaxation, descend_into=True, active=True): + for v in r.get_rhs_vars(): + if v not in var_to_relaxation_map: + var_to_relaxation_map[v] = list() + var_to_relaxation_map[v].append(r) + + num_stages = relaxation.num_stages() + + for stage in range(num_stages): + stage_blocks = list(relaxation.stage_blocks(stage)) + for block in stage_blocks: + vars_to_tighten = vars_to_tighten_by_block[block] + if obbt_method == OBBTMethod.FULL_SPACE or block.is_leaf(): + dbt_info.num_vars_to_tighten += 2 * len(vars_to_tighten) + else: + dbt_info.num_coupling_vars_to_tighten += 2 * len(vars_to_tighten) + + if obbt_method == OBBTMethod.FULL_SPACE: + all_vars_to_tighten = ComponentSet() + for block, block_vars_to_tighten in vars_to_tighten_by_block.items(): + all_vars_to_tighten.update(block_vars_to_tighten) + if filter_method == FilterMethod.AGGRESSIVE: + logger.debug('starting full space filter') + res = aggressive_filter(candidate_variables=all_vars_to_tighten, relaxation=relaxation, + solver=solver, tolerance=1e-4, objective_bound=objective_bound) + full_space_lb_vars, full_space_ub_vars = res + logger.debug('finished full space filter') + else: + full_space_lb_vars = all_vars_to_tighten + full_space_ub_vars = all_vars_to_tighten + else: + full_space_lb_vars = None + full_space_ub_vars = None + + for stage in range(num_stages): + logger.info(f'Performing DBT on stage {stage+1} of {num_stages}') + if time.time() - t0 >= time_limit: + break + + stage_blocks = list(relaxation.stage_blocks(stage)) + logger.debug('DBT stage {0} of {1} with {1} blocks'.format(stage, num_stages, len(stage_blocks))) + + for block_ndx, block in enumerate(stage_blocks): + logger.info(f'performing DBT on block {block_ndx+1} of {len(stage_blocks)} in stage {stage+1}') + if time.time() - t0 >= time_limit: + break + + if obbt_method in {OBBTMethod.LEAVES, OBBTMethod.FULL_SPACE} and (not block.is_leaf()): + continue + if obbt_method == OBBTMethod.FULL_SPACE: + block_to_tighten_with = relaxation + _ub = objective_bound + else: + block_to_tighten_with = block + if stage == 0: + _ub = objective_bound + else: + _ub = None + + vars_to_tighten = vars_to_tighten_by_block[block] + + if filter_method == FilterMethod.AGGRESSIVE: + logger.debug('starting filter') + if obbt_method == OBBTMethod.FULL_SPACE: + lb_vars = ComponentSet([v for v in vars_to_tighten if v in full_space_lb_vars]) + ub_vars = ComponentSet([v for v in vars_to_tighten if v in full_space_ub_vars]) + else: + res = aggressive_filter(candidate_variables=vars_to_tighten, relaxation=block_to_tighten_with, + solver=solver, tolerance=1e-4, objective_bound=_ub) + lb_vars, ub_vars = res + if block.is_leaf(): + dbt_info.num_vars_filtered += 2*len(vars_to_tighten) - len(lb_vars) - len(ub_vars) + else: + dbt_info.num_coupling_vars_filtered += 2*len(vars_to_tighten) - len(lb_vars) - len(ub_vars) + logger.debug('done filtering') + else: + lb_vars = list(vars_to_tighten) + ub_vars = list(vars_to_tighten) + + logger.debug(f'performing OBBT (LB) on variables {str([str(i) for i in lb_vars])}') + res = normal_obbt(block_to_tighten_with, solver=solver, varlist=lb_vars, + objective_bound=_ub, with_progress_bar=with_progress_bar, + direction='lbs', time_limit=(time_limit - (time.time() - t0)), + update_bounds=False, parallel=parallel, collect_obbt_info=True, + progress_bar_string=f'DBT LBs Stage {stage+1} of {num_stages} Block {block_ndx+1} of {len(stage_blocks)}') + lower, unused_upper, obbt_info = res + if block.is_leaf(): + dbt_info.num_vars_attempted += obbt_info.num_problems_attempted + dbt_info.num_vars_successful += obbt_info.num_successful_problems + else: + dbt_info.num_coupling_vars_attempted += obbt_info.num_problems_attempted + dbt_info.num_coupling_vars_successful += obbt_info.num_successful_problems + + logger.debug('done tightening lbs') + + logger.debug(f'performing OBBT (UB) on variables {str([str(i) for i in ub_vars])}') + res = normal_obbt(block_to_tighten_with, solver=solver, varlist=ub_vars, + objective_bound=_ub, with_progress_bar=with_progress_bar, + direction='ubs', time_limit=(time_limit - (time.time() - t0)), + update_bounds=False, parallel=parallel, collect_obbt_info=True, + progress_bar_string=f'DBT UBs Stage {stage+1} of {num_stages} Block {block_ndx+1} of {len(stage_blocks)}') + + unused_lower, upper, obbt_info = res + if block.is_leaf(): + dbt_info.num_vars_attempted += obbt_info.num_problems_attempted + dbt_info.num_vars_successful += obbt_info.num_successful_problems + else: + dbt_info.num_coupling_vars_attempted += obbt_info.num_problems_attempted + dbt_info.num_coupling_vars_successful += obbt_info.num_successful_problems + + _update_var_bounds(varlist=lb_vars, new_lower_bounds=lower, + new_upper_bounds=unused_upper, feasibility_tol=feasibility_tol, + safety_tol=safety_tol, max_acceptable_bound=max_acceptable_bound) + + _update_var_bounds(varlist=ub_vars, new_lower_bounds=unused_lower, + new_upper_bounds=upper, feasibility_tol=feasibility_tol, + safety_tol=safety_tol, max_acceptable_bound=max_acceptable_bound) + + if update_relaxations_between_stages: + # this is needed to ensure consistency for parallel computing; this accounts + # for side effects from the OBBT problems; in particular, if the solver ever + # rebuilds relaxations, then the processes could become out of sync without + # this code + all_tightened_vars = ComponentSet(lb_vars) + all_tightened_vars.update(ub_vars) + for v in all_tightened_vars: + if v in var_to_relaxation_map: + for r in var_to_relaxation_map[v]: + r.rebuild() + + logger.debug('done tightening ubs') + + if not block.is_leaf(): + for c in block.linking_constraints.values(): + fbbt(c) + + return dbt_info + + +def push_integers(block): + """ + Parameters + ---------- + block: pyomo.core.base.block._BlockData + The block for which integer variables should be relaxed. + + Returns + ------- + relaxed_binary_vars: ComponentSet of pyomo.core.base.var._GeneralVarData + relaxed_integer_vars: ComponentSet or pyomo.core.base.var._GeneralVarData + """ + relaxed_binary_vars = ComponentSet() + relaxed_integer_vars = ComponentSet() + for v in block.component_data_objects(pe.Var, descend_into=True, sort=True): + if v.fixed: + continue + if v.is_binary(): + relaxed_binary_vars.add(v) + orig_lb = v.lb + orig_ub = v.ub + v.domain = pe.Reals + v.setlb(orig_lb) + v.setub(orig_ub) + elif v.is_integer(): + relaxed_integer_vars.add(v) + v.domain = pe.Reals + + return relaxed_binary_vars, relaxed_integer_vars + + +def pop_integers(relaxed_binary_vars, relaxed_integer_vars): + for v in relaxed_binary_vars: + v.domain = pe.Binary + for v in relaxed_integer_vars: + v.domain = pe.Integers + + +def perform_dbt_with_integers_relaxed(relaxation, solver, obbt_method=OBBTMethod.DECOMPOSED, + filter_method=FilterMethod.AGGRESSIVE, time_limit=math.inf, + objective_bound=None, with_progress_bar=False, parallel=False, + vars_to_tighten_by_block=None, feasibility_tol=0, + integer_tol=1e-2, safety_tol=0, max_acceptable_bound=math.inf): + """ + This function performs optimization-based bounds tightening (OBBT) with a decomposition scheme. + However, all OBBT problems are solved with the binary and integer variables relaxed. + + Parameters + ---------- + relaxation: dbt.decomp.decompose.TreeBlockData + The relaxation to use for OBBT. + solver: pyomo solver object + The solver to use for the OBBT problems. + obbt_method: OBBTMethod + An enum member from OBBTMethod. The default is OBBTMethod.DECOMPOSED. If obbt_method + is OBBTMethod.DECOMPOSED, then only the coupling variables in the linking constraints + will be tightened with non-leaf blocks in relaxation. The nonlinear and discrete + variables will only be tightened with the leaf blocks in relaxation. See the + documentation on TreeBlockData for more details on leaf blocks. If the method is + OBBTMethod.FULL_SPACE, then all of the nonlinear and discrete variables will + be tightened with the root block from relaxation. If the method is + OBBTMethod.LEAVES, then the nonlinear and discrete variables will be tightened + with the leaf blocks from relaxation (none of the coupling variables will be + tightened). + filter_method: FilterMethod + An enum member from FilterMethod. The default is FilterMethod.AGGRESSIVE. If + filter_method is FilterMethod.AGGRESSIVE, then aggressive filtering will be + performed at every stage of OBBT using the + coramin.domain_reduction.filters.aggressive_filter function which is based on + + Gleixner, Ambros M., et al. "Three enhancements for + optimization-based bound tightening." Journal of Global + Optimization 67.4 (2017): 731-757. + + If filter_method is FilterMethod.NONE, then no filtering will be performed. + time_limit: float + If the time spent in this function exceeds time_limit, OBBT will be terminated + early. + objective_bound: float + A lower or upper bound on the objective. If this is not None, then a constraint will be added to the + bounds tightening problems constraining the objective to be less than/greater than objective_bound. + with_progress_bar: bool + parallel: bool + If True, then OBBT will automatically be performed in parallel if mpirun or mpiexec was used; + If False, then OBBT will not run in parallel even if mpirun or mpiexec was used; + vars_to_tighten_by_block: dict + Dictionary mapping TreeBlockData to ComponentSet. This dictionary indicates which variables + should be tightened with which parts of the TreeBlockData. If None is passed (default=None), + then, the function collect_vars_to_tighten_by_block is used to get the dict. + feasibility_tol: float + If the lower bound computed for a variable is larger than the computed upper bound by more than + feasibility_tol, then an error is raised. If the computed lower bound is larger than the computed + upper bound, but by less than feasibility_tol, then the computed lower bound is decreased by + feasibility tol (but will not be set lower than the original lower bound) and the computed upper + bound is increased by feasibility_tol (but will not be set higher than the original upper bound). + integer_tol: float + If the lower bound computed for an integer variable is greater than the largest integer less than + the computed lower bound by more than integer_tol, then the lower bound is increased to the smallest + integer greater than the computed lower bound. Similar logic holds for the upper bound. + safety_tol: float + Computed lower bounds will be decreased by max(safety_tol, safety_tol*abs(new_lb) and + computed upper bounds will be increased by max(safety_tol, safety_tol*abs(new_ub) where + new_lb and new_ub are the bounds computed from OBBT/DBT. The purpose of this is to + account for numerical error in the solution of the OBBT problems and to avoid cutting + off valid portions of the feasible region. + max_acceptable_bound: float + If the upper bound computed for a variable is larger than max_acceptable_bound, then the + computed bound will be rejected. If the lower bound computed for a variable is less than + -max_acceptable_bound, then the computed bound will be rejected. + + Returns + ------- + dbt_info: DBTInfo + """ + assert obbt_method in OBBTMethod + if vars_to_tighten_by_block is None: + if obbt_method == OBBTMethod.DECOMPOSED: + _method = 'dbt' + elif obbt_method == OBBTMethod.FULL_SPACE: + _method = 'full_space' + else: + _method = 'leaves' + vars_to_tighten_by_block = collect_vars_to_tighten_by_block(relaxation, _method) + + relaxed_binary_vars, relaxed_integer_vars = push_integers(relaxation) + + dbt_info = perform_dbt(relaxation=relaxation, + solver=solver, + obbt_method=obbt_method, + filter_method=filter_method, + time_limit=time_limit, + objective_bound=objective_bound, + with_progress_bar=with_progress_bar, + parallel=parallel, + vars_to_tighten_by_block=vars_to_tighten_by_block, + feasibility_tol=feasibility_tol, + safety_tol=safety_tol, + max_acceptable_bound=max_acceptable_bound) + + pop_integers(relaxed_binary_vars, relaxed_integer_vars) + + for v in (list(relaxed_binary_vars) + list(relaxed_integer_vars)): + lb = v.lb + ub = v.ub + if lb is None: + lb = -math.inf + if ub is None: + ub = math.inf + if lb > -math.inf: + lb = max(math.floor(lb), math.ceil(lb - integer_tol)) + if ub < math.inf: + ub = min(math.ceil(ub), math.floor(ub + integer_tol)) + if lb > -math.inf: + v.setlb(lb) + if ub < math.inf: + v.setub(ub) + + return dbt_info diff --git a/pyomo/contrib/coramin/domain_reduction/filters.py b/pyomo/contrib/coramin/domain_reduction/filters.py new file mode 100644 index 00000000000..d0fd3ccb10a --- /dev/null +++ b/pyomo/contrib/coramin/domain_reduction/filters.py @@ -0,0 +1,193 @@ +from pyomo.common.collections import ComponentSet +from coramin.domain_reduction.obbt import _bt_prep, _bt_cleanup +import pyomo.environ as pe +from pyomo.core.expr.numeric_expr import LinearExpression +import logging +from pyomo.contrib import appsi +from pyomo.core.base.var import _GeneralVarData +from pyomo.core.base.block import _BlockData +from typing import Sequence, Optional, Union + + +logger = logging.getLogger(__name__) + + +def filter_variables_from_solution(candidate_variables_at_relaxation_solution, tolerance=1e-6): + """ + This function takes a set of candidate variables for OBBT and filters out + the variables that are at their bounds in the provided solution to the + relaxation. See + + Gleixner, Ambros M., et al. "Three enhancements for + optimization-based bound tightening." Journal of Global + Optimization 67.4 (2017): 731-757. + + for details on why this works. The basic idea is that if x = xl is + feasible for the relaxation that will be used for OBBT, then + minimizing x subject to that relaxation is guaranteed to result in + an optimal solution of x* = xl. + + This function simply loops through + candidate_variables_at_relaxation_solution and specifies which + variables should be minimized and which variables should be + maximized with OBBT. + + Parameters + ---------- + candidate_variables_at_relaxation_solution: iterable of _GeneralVarData + This should be an iterable of the variables which are candidates + for OBBT. The values of the variables should be feasible for the + relaxation that would be used to perform OBBT on the variables. + tolerance: float + A float greater than or equal to zero. If the value of the variable + is within tolerance of its lower bound, then that variable is filtered + from the set of variables that should be minimized for OBBT. The same + is true for upper bounds and variables that should be maximized. + + Returns + ------- + vars_to_minimize: ComponentSet of _GeneralVarData + variables that should be considered for minimization + vars_to_maximize: ComponentSet of _GeneralVarData + variables that should be considered for maximization + """ + candidate_vars = ComponentSet(candidate_variables_at_relaxation_solution) + vars_to_minimize = ComponentSet() + vars_to_maximize = ComponentSet() + + for v in candidate_vars: + if (not v.has_lb()) or (v.value - v.lb > tolerance): + vars_to_minimize.add(v) + if (not v.has_ub()) or (v.ub - v.value > tolerance): + vars_to_maximize.add(v) + + return vars_to_minimize, vars_to_maximize + + +def aggressive_filter( + candidate_variables: Sequence[_GeneralVarData], + relaxation: _BlockData, + solver: Union[appsi.base.Solver, appsi.base.PersistentSolver], + tolerance: float = 1e-6, + objective_bound: Optional[float] = None, + max_iter: int = 10, + improvement_threshold: int = 5 +): + """ + This function takes a set of candidate variables for OBBT and filters out + the variables for which it does not make senese to perform OBBT on. See + + Gleixner, Ambros M., et al. "Three enhancements for + optimization-based bound tightening." Journal of Global + Optimization 67.4 (2017): 731-757. + + for details. The basic idea is that if x = xl is + feasible for the relaxation that will be used for OBBT, then + minimizing x subject to that relaxation is guaranteed to result in + an optimal solution of x* = xl. + + This function solves a series of optimization problems to try to + filter as many variables as possible. + + Parameters + ---------- + candidate_variables: iterable of _GeneralVarData + This should be an iterable of the variables which are candidates + for OBBT. + relaxation: Block + a convex relaxation + solver: appsi.base.Solver + tolerance: float + A float greater than or equal to zero. If the value of the variable + is within tolerance of its lower bound, then that variable is filtered + from the set of variables that should be minimized for OBBT. The same + is true for upper bounds and variables that should be maximized. + objective_bound: float + Primal bound for the objective + max_iter: int + Maximum number of iterations + improvement_threshold: int + If the number of filtered variables is less than improvement_threshold, then + the filtering is terminated + + Returns + ------- + vars_to_minimize: list of _GeneralVarData + variables that should be considered for minimization + vars_to_maximize: list of _GeneralVarData + variables that should be considered for maximization + """ + vars_to_minimize = ComponentSet(candidate_variables) + vars_to_maximize = ComponentSet(candidate_variables) + if len(candidate_variables) == 0: + return vars_to_minimize, vars_to_maximize + + tmp = _bt_prep(model=relaxation, solver=solver, objective_bound=objective_bound) + initial_var_values, deactivated_objectives, orig_update_config, orig_config = tmp + + vars_unbounded_from_below = ComponentSet() + vars_unbounded_from_above = ComponentSet() + for v in list(vars_to_minimize): + if v.lb is None: + vars_unbounded_from_below.add(v) + vars_to_minimize.remove(v) + for v in list(vars_to_maximize): + if v.ub is None: + vars_unbounded_from_above.add(v) + vars_to_maximize.remove(v) + + for _set in [vars_to_minimize, vars_to_maximize]: + for _iter in range(max_iter): + if _set is vars_to_minimize: + obj_coefs = [1 for v in _set] + else: + obj_coefs = [-1 for v in _set] + obj_vars = list(_set) + relaxation.__filter_obj = pe.Objective(expr=LinearExpression(linear_coefs=obj_coefs, linear_vars=obj_vars)) + if solver.is_persistent(): + solver.set_objective(relaxation.__filter_obj) + solver.config.load_solution = False + res = solver.solve(relaxation) + if res.termination_condition == appsi.base.TerminationCondition.optimal: + res.solution_loader.load_vars() + success = True + else: + success = False + del relaxation.__filter_obj + + if not success: + break + + num_filtered = 0 + for v in list(_set): + should_filter = False + if _set is vars_to_minimize: + if v.value - v.lb <= tolerance: + should_filter = True + else: + if v.ub - v.value <= tolerance: + should_filter = True + if should_filter: + num_filtered += 1 + _set.remove(v) + logger.debug('filtered {0} vars on iter {1}'.format(num_filtered, _iter)) + + if len(_set) == 0: + break + if num_filtered < improvement_threshold: + break + + for v in vars_unbounded_from_below: + vars_to_minimize.add(v) + for v in vars_unbounded_from_above: + vars_to_maximize.add(v) + + _bt_cleanup( + model=relaxation, solver=solver, vardatalist=None, + initial_var_values=initial_var_values, + deactivated_objectives=deactivated_objectives, + orig_update_config=orig_update_config, orig_config=orig_config, + lower_bounds=None, upper_bounds=None + ) + + return vars_to_minimize, vars_to_maximize diff --git a/pyomo/contrib/coramin/domain_reduction/obbt.py b/pyomo/contrib/coramin/domain_reduction/obbt.py new file mode 100644 index 00000000000..8b4248411cd --- /dev/null +++ b/pyomo/contrib/coramin/domain_reduction/obbt.py @@ -0,0 +1,522 @@ +import pyomo.environ as pyo +from pyomo.core.base.var import _GeneralVarData +from pyomo.core.base.PyomoModel import ConcreteModel +import warnings +from pyomo.common.collections import ComponentMap +from pyomo.core.kernel.objective import minimize, maximize +from pyomo.contrib import appsi +import logging +import traceback +import numpy as np +import math +import time +from typing import Union, Sequence, Optional, List +from pyomo.core.base.objective import ScalarObjective, _GeneralObjectiveData +try: + import coramin.utils.mpi_utils as mpiu + mpi_available = True +except ImportError: + mpi_available = False +try: + from tqdm import tqdm +except ImportError: + pass + + +logger = logging.getLogger(__name__) + + +class OBBTInfo(object): + def __init__(self): + self.total_num_problems = None + self.num_problems_attempted = None + self.num_successful_problems = None + + +def _bt_cleanup( + model, solver: Union[appsi.base.Solver, appsi.base.PersistentSolver], + vardatalist: Optional[List[_GeneralVarData]], + initial_var_values, deactivated_objectives, orig_update_config, orig_config, + lower_bounds: Optional[Sequence[float]] = None, + upper_bounds: Optional[Sequence[float]] = None +): + """ + Cleanup the changes made to the model during bounds tightening. + Reactivate any deactivated objectives. + Remove an objective upper bound constraint if it was added. + If lower_bounds or upper_bounds is provided, update the bounds of the variables in self.vars_to_tighten. + + Parameters + ---------- + model: pyo.ConcreteModel or pyo.Block + solver: Union[appsi.base.Solver, appsi.base.PersistentSolver] + vardatalist: List of _GeneralVarData + initial_var_values: ComponentMap + deactivated_objectives: list of _GeneralObjectiveData + orig_update_config: appsi.base.UpdateConfig + orig_config: appsi.base.SolverConfig + lower_bounds: Sequence of float + Only needed if you want to update the bounds of the variables. Should be in the same order as + self.vars_to_tighten. + upper_bounds: Sequence of float + Only needed if you want to update the bounds of the variables. Should be in the same order as + self.vars_to_tighten. + """ + for v in model.component_data_objects(ctype=pyo.Var, active=None, sort=True, descend_into=True): + v.set_value(initial_var_values[v], skip_validation=True) + + if hasattr(model, '__objective_ineq'): + if solver.is_persistent(): + solver.remove_constraints([model.__objective_ineq]) + del model.__objective_ineq + + # reactivate the objectives that we deactivated + for obj in deactivated_objectives: + obj.activate() + if solver.is_persistent(): + solver.set_objective(obj) + + if lower_bounds is not None and upper_bounds is not None: + for i, v in enumerate(vardatalist): + lb = lower_bounds[i] + ub = upper_bounds[i] + v.setlb(lb) + v.setub(ub) + elif lower_bounds is not None: + for i, v in enumerate(vardatalist): + lb = lower_bounds[i] + v.setlb(lb) + elif upper_bounds is not None: + for i, v in enumerate(vardatalist): + ub = upper_bounds[i] + v.setub(ub) + if vardatalist is not None and solver.is_persistent(): + solver.update_variables(vardatalist) + + if solver.is_persistent(): + solver.update_config.check_for_new_or_removed_constraints = \ + orig_update_config.check_for_new_or_removed_constraints + solver.update_config.check_for_new_or_removed_vars = \ + orig_update_config.check_for_new_or_removed_vars + solver.update_config.check_for_new_or_removed_params = \ + orig_update_config.check_for_new_or_removed_params + solver.update_config.check_for_new_objective = \ + orig_update_config.check_for_new_objective + solver.update_config.update_constraints = \ + orig_update_config.update_constraints + solver.update_config.update_vars = \ + orig_update_config.update_vars + solver.update_config.update_params = \ + orig_update_config.update_params + solver.update_config.update_named_expressions = \ + orig_update_config.update_named_expressions + solver.update_config.update_objective = \ + orig_update_config.update_objective + solver.update_config.treat_fixed_vars_as_params = \ + orig_update_config.treat_fixed_vars_as_params + + solver.config.stream_solver = orig_config.stream_solver + solver.config.load_solution = orig_config.load_solution + + +def _single_solve(v, model, solver: Union[appsi.base.Solver, appsi.base.PersistentSolver], lb_or_ub, obbt_info): + obbt_info.num_problems_attempted += 1 + # solve for lower var bound + if lb_or_ub == 'lb': + model.__obj_bounds_tightening = ScalarObjective(expr=v, sense=pyo.minimize) + else: + assert lb_or_ub == 'ub' + model.__obj_bounds_tightening = ScalarObjective(expr=v, sense=pyo.maximize) + + if solver.is_persistent(): + solver.set_objective(model.__obj_bounds_tightening) + results = solver.solve(model) + if results.termination_condition == appsi.base.TerminationCondition.optimal: + obbt_info.num_successful_problems += 1 + if results.best_objective_bound is not None and math.isfinite(results.best_objective_bound): + new_bnd = results.best_objective_bound + elif results.termination_condition == appsi.base.TerminationCondition.optimal: + new_bnd = results.best_feasible_objective # assumes the problem is convex + else: + new_bnd = None + msg = f'Warning: Bounds tightening for lb for var {str(v)} was unsuccessful. Termination condition: {results.termination_condition}; The lb was not changed.' + logger.warning(msg) + + if lb_or_ub == 'lb': + orig_lb = pyo.value(v.lb) + if new_bnd is None: + new_bnd = orig_lb + elif v.has_lb(): + if new_bnd < orig_lb: + new_bnd = orig_lb + else: + orig_ub = pyo.value(v.ub) + if new_bnd is None: + new_bnd = orig_ub + elif v.has_ub(): + if new_bnd > orig_ub: + new_bnd = orig_ub + + if new_bnd is None: + # Need nan instead of None for MPI communication; This is appropriately handled in perform_obbt(). + new_bnd = np.nan + + # remove the objective function + del model.__obj_bounds_tightening + + return new_bnd + + +def _tighten_bnds(model, solver, vardatalist, lb_or_ub, obbt_info, with_progress_bar=False, time_limit=math.inf, progress_bar_string=None): + """ + Tighten the lower bounds of all variables in vardatalist (or self.vars_to_tighten if vardatalist is None). + + Parameters + ---------- + model: pyo.ConcreteModel or pyo.Block + solver: pyomo solver object + vardatalist: list of _GeneralVarData + lb_or_ub: str + 'lb' or 'ub' + time_limit: float + + Returns + ------- + new_bounds: list of float + """ + # solve for the new bounds + t0 = time.time() + new_bounds = list() + + obbt_info.total_num_problems += len(vardatalist) + + if with_progress_bar: + if progress_bar_string is None: + if lb_or_ub == 'lb': + bnd_str = 'LBs' + else: + bnd_str = 'UBs' + bnd_str = 'OBBT ' + bnd_str + else: + bnd_str = progress_bar_string + if mpi_available: + tqdm_position = mpiu.MPI.COMM_WORLD.Get_rank() + else: + tqdm_position = 0 + for v in tqdm(vardatalist, ncols=100, desc=bnd_str, leave=False, position=tqdm_position): + if time.time() - t0 > time_limit: + if lb_or_ub == 'lb': + if v.lb is None: + new_bounds.append(np.nan) + else: + new_bounds.append(pyo.value(v.lb)) + else: + if v.ub is None: + new_bounds.append(np.nan) + else: + new_bounds.append(pyo.value(v.ub)) + else: + new_bnd = _single_solve(v=v, model=model, solver=solver, + lb_or_ub=lb_or_ub, + obbt_info=obbt_info) + new_bounds.append(new_bnd) + else: + for v in vardatalist: + if time.time() - t0 > time_limit: + if lb_or_ub == 'lb': + if v.lb is None: + new_bounds.append(np.nan) + else: + new_bounds.append(pyo.value(v.lb)) + else: + if v.ub is None: + new_bounds.append(np.nan) + else: + new_bounds.append(pyo.value(v.ub)) + else: + new_bnd = _single_solve(v=v, model=model, solver=solver, + lb_or_ub=lb_or_ub, + obbt_info=obbt_info) + new_bounds.append(new_bnd) + + return new_bounds + + +def _bt_prep(model, solver, objective_bound=None): + """ + Prepare the model for bounds tightening. + Gather the variable values to load back in after bounds tightening. + Deactivate any active objectives. + If objective_ub is not None, then add a constraint forcing the objective to be less than objective_ub + + Parameters + ---------- + model : pyo.ConcreteModel or pyo.Block + The model object that will be used for bounds tightening. + objective_bound : float + The objective value for the current best upper bound incumbent + + Returns + ------- + initial_var_values: ComponentMap + deactivated_objectives: list + orig_update_config: appsi.base.UpdateConfig + orig_config: appsi.base.SolverConfig + """ + + if solver.is_persistent(): + orig_update_config = solver.update_config() + solver.update_config.check_for_new_or_removed_constraints = False + solver.update_config.check_for_new_or_removed_vars = False + solver.update_config.check_for_new_or_removed_params = False + solver.update_config.check_for_new_objective = False + solver.update_config.update_constraints = False + solver.update_config.update_vars = False + solver.update_config.update_params = False + solver.update_config.update_named_expressions = False + solver.update_config.update_objective = False + solver.update_config.treat_fixed_vars_as_params = True + else: + orig_update_config = None + + orig_config = solver.config() + solver.config.stream_solver = False + solver.config.load_solution = False + + if solver.is_persistent(): + solver.set_instance(model) + + initial_var_values = ComponentMap() + for v in model.component_data_objects(ctype=pyo.Var, active=None, sort=True, descend_into=True): + initial_var_values[v] = v.value + + deactivated_objectives = list() + for obj in model.component_data_objects(pyo.Objective, active=True, sort=True, descend_into=True): + deactivated_objectives.append(obj) + obj.deactivate() + + # add inequality bound on objective functions if required + # obj.expr <= objective_ub + if objective_bound is not None and math.isfinite(objective_bound): + if len(deactivated_objectives) != 1: + e = 'BoundsTightener: When providing objective_ub,' + \ + ' the model must have one and only one objective function.' + logger.error(e) + raise ValueError(e) + original_obj = deactivated_objectives[0] + if original_obj.sense == minimize: + model.__objective_ineq = \ + pyo.Constraint(expr=original_obj.expr <= objective_bound) + else: + assert original_obj.sense == maximize + model.__objective_ineq = pyo.Constraint(expr=original_obj.expr >= objective_bound) + if solver.is_persistent(): + solver.add_constraints([model.__objective_ineq]) + + return initial_var_values, deactivated_objectives, orig_update_config, orig_config + + +def _build_vardatalist(model, varlist=None, warning_threshold=0): + """ + Convert a list of pyomo variables to a list of SimpleVar and _GeneralVarData. If varlist is none, builds a + list of all variables in the model. The new list is stored in the vars_to_tighten attribute. + + Parameters + ---------- + model: ConcreteModel + varlist: None or list of pyo.Var + warning_threshold: float + The threshold below which a warning is raised when attempting to perform OBBT on variables whose + ub - lb < warning_threshold. + """ + vardatalist = None + + # if the varlist is None, then assume we want all the active variables + if varlist is None: + raise NotImplementedError('Still need to do this.') + elif isinstance(varlist, pyo.Var): + # user provided a variable, not a list of variables. Let's work with it anyway + varlist = [varlist] + + if vardatalist is None: + # expand any indexed components in the list to their + # component data objects + vardatalist = list() + for v in varlist: + if v.is_indexed(): + vardatalist.extend(v.values()) + else: + vardatalist.append(v) + + # remove from vardatalist if the variable is fixed (maybe there is a better way to do this) + corrected_vardatalist = [] + for v in vardatalist: + if not v.is_fixed(): + if v.has_lb() and v.has_ub(): + if v.ub - v.lb < warning_threshold: + e = 'Warning: Tightening a variable with ub - lb is less than {threshold}: {v}, lb: {lb}, ub: {ub}'.format(threshold=warning_threshold, v=v, lb=v.lb, ub=v.ub) + logger.warning(e) + warnings.warn(e) + corrected_vardatalist.append(v) + + return corrected_vardatalist + + +def perform_obbt(model, solver, varlist=None, objective_bound=None, update_bounds=True, with_progress_bar=False, + direction='both', time_limit=math.inf, parallel=True, collect_obbt_info=False, + warning_threshold=0, progress_bar_string=None): + """ + Perform optimization-based bounds tighening on the variables in varlist subject to the constraints in model. + + Parameters + ---------- + model: pyo.ConcreteModel or pyo.Block + The model to be used for bounds tightening + solver: appsi.base.PersistentSolver + The solver to be used for bounds tightening. + varlist: list of pyo.Var + The variables for which OBBT should be performed. If varlist is None, then we attempt to automatically + detect which variables need tightened. + objective_bound: float + A lower or upper bound on the objective. If this is not None, then a constraint will be added to the + bounds tightening problems constraining the objective to be less than/greater than objective_bound. + update_bounds: bool + If True, then the variable bounds will be updated + with_progress_bar: bool + direction: str + Options are 'both', 'lbs', or 'ubs' + time_limit: float + The maximum amount of time to be spent performing OBBT + parallel: bool + If True, then OBBT will automatically be performed in parallel if mpirun or mpiexec was used; + If False, then OBBT will not run in parallel even if mpirun or mpiexec was used; + warning_threshold: float + The threshold below which a warning is issued when attempting to perform OBBT on variables whose + ub - lb < warning_threshold. + + Returns + ------- + lower_bounds: list of float + upper_bounds: list of float + obbt_info: OBBTInfo + + """ + if not isinstance(solver, appsi.base.Solver): + raise ValueError('Coramin requires an Appsi solver interface') + + obbt_info = OBBTInfo() + obbt_info.total_num_problems = 0 + obbt_info.num_problems_attempted = 0 + obbt_info.num_successful_problems = 0 + + t0 = time.time() + initial_var_values, deactivated_objectives, orig_update_config, orig_config = _bt_prep(model=model, solver=solver, objective_bound=objective_bound) + + vardata_list = _build_vardatalist(model=model, varlist=varlist, warning_threshold=warning_threshold) + if mpi_available and parallel: + mpi_interface = mpiu.MPIInterface() + alloc_map = mpiu.MPIAllocationMap(mpi_interface, len(vardata_list)) + local_vardata_list = alloc_map.local_list(vardata_list) + else: + local_vardata_list = vardata_list + + exc = None + try: + if direction in {'both', 'lbs'}: + local_lower_bounds = _tighten_bnds(model=model, solver=solver, + vardatalist=local_vardata_list, + lb_or_ub='lb', + obbt_info=obbt_info, + with_progress_bar=with_progress_bar, + time_limit=(time_limit - (time.time() - t0)), + progress_bar_string=progress_bar_string) + else: + local_lower_bounds = list() + for v in local_vardata_list: + if v.lb is None: + local_lower_bounds.append(np.nan) + else: + local_lower_bounds.append(pyo.value(v.lb)) + if direction in {'both', 'ubs'}: + local_upper_bounds = _tighten_bnds(model=model, solver=solver, + vardatalist=local_vardata_list, + lb_or_ub='ub', + obbt_info=obbt_info, + with_progress_bar=with_progress_bar, + time_limit=(time_limit - (time.time() - t0)), + progress_bar_string=progress_bar_string) + else: + local_upper_bounds = list() + for v in local_vardata_list: + if v.ub is None: + local_upper_bounds.append(np.nan) + else: + local_upper_bounds.append(pyo.value(v.ub)) + status = 1 + msg = None + except Exception as err: + exc = err + tb = traceback.format_exc() + status = 0 + msg = str(tb) + + if mpi_available and parallel: + local_status = np.array([status], dtype='i') + global_status = np.array([0 for i in range(mpiu.MPI.COMM_WORLD.Get_size())], dtype='i') + mpiu.MPI.COMM_WORLD.Allgatherv([local_status, mpiu.MPI.INT], [global_status, mpiu.MPI.INT]) + if not np.all(global_status): + messages = mpi_interface.comm.allgather(msg) + msg = None + for m in messages: + if m is not None: + msg = m + logger.error('An error was raised in one or more processes:\n' + msg) + raise mpiu.MPISyncError('An error was raised in one or more processes:\n' + msg) + else: + if status != 1: + logger.error('An error was raised during OBBT:\n' + msg) + raise exc + + if mpi_available and parallel: + global_lower = alloc_map.global_list_float64(local_lower_bounds) + global_upper = alloc_map.global_list_float64(local_upper_bounds) + obbt_info.total_num_problems = mpiu.MPI.COMM_WORLD.allreduce(obbt_info.total_num_problems) + obbt_info.num_problems_attempted = mpiu.MPI.COMM_WORLD.allreduce(obbt_info.num_problems_attempted) + obbt_info.num_successful_problems = mpiu.MPI.COMM_WORLD.allreduce(obbt_info.num_successful_problems) + else: + global_lower = local_lower_bounds + global_upper = local_upper_bounds + + tmp = list() + for i in global_lower: + if np.isnan(i): + tmp.append(None) + else: + tmp.append(float(i)) + global_lower = tmp + + tmp = list() + for i in global_upper: + if np.isnan(i): + tmp.append(None) + else: + tmp.append(float(i)) + global_upper = tmp + + _lower_bounds = None + _upper_bounds = None + if update_bounds: + _lower_bounds = global_lower + _upper_bounds = global_upper + _bt_cleanup( + model=model, solver=solver, vardatalist=vardata_list, + initial_var_values=initial_var_values, + deactivated_objectives=deactivated_objectives, + orig_update_config=orig_update_config, orig_config=orig_config, + lower_bounds=_lower_bounds, upper_bounds=_upper_bounds + ) + + if collect_obbt_info: + return global_lower, global_upper, obbt_info + else: + return global_lower, global_upper diff --git a/pyomo/contrib/coramin/domain_reduction/tests/test_dbt.py b/pyomo/contrib/coramin/domain_reduction/tests/test_dbt.py new file mode 100644 index 00000000000..329139cea9d --- /dev/null +++ b/pyomo/contrib/coramin/domain_reduction/tests/test_dbt.py @@ -0,0 +1,825 @@ +from coramin.domain_reduction.dbt import TreeBlock, TreeBlockError, convert_pyomo_model_to_bipartite_graph, \ + _VarNode, _ConNode, _RelNode, split_metis, num_cons_in_graph, collect_vars_to_tighten_by_block, decompose_model, \ + perform_dbt, OBBTMethod, FilterMethod +import unittest +import pyomo.environ as pe +import coramin +from networkx import is_bipartite +from pyomo.common.collections import ComponentSet +from networkx import Graph +from pyomo.core.expr.visitor import identify_variables +from pyomo.core.expr import differentiate +from egret.thirdparty.get_pglib_opf import get_pglib_opf +from egret.data.model_data import ModelData +from egret.models.acopf import create_psv_acopf_model +import os +from coramin.utils.pyomo_utils import get_objective +import filecmp +from pyomo.contrib import appsi +import pytest + + +class TestTreeBlock(unittest.TestCase): + def test_tree_block(self): + b = TreeBlock(concrete=True) + with self.assertRaises(TreeBlockError): + b.is_leaf() + with self.assertRaises(TreeBlockError): + b.x = pe.Var() + with self.assertRaises(TreeBlockError): + children = b.children + with self.assertRaises(TreeBlockError): + linking_constraints = b.linking_constraints + with self.assertRaises(TreeBlockError): + num_stages = b.num_stages() + with self.assertRaises(TreeBlockError): + stage_blocks = list(b.stage_blocks(0)) + with self.assertRaises(TreeBlockError): + stage = b.get_block_stage(b) + b.setup(children_keys=list()) + self.assertTrue(b.is_leaf()) + b.x = pe.Var() # make sure we can add components just like a regular block + b.x.setlb(-1) + with self.assertRaises(TreeBlockError): + linking_constraints = b.linking_constraints + with self.assertRaises(TreeBlockError): + children = b.children + self.assertEqual(b.num_stages(), 1) + stage0_blocks = list(b.stage_blocks(0)) + self.assertEqual(len(stage0_blocks), 1) + self.assertIs(stage0_blocks[0], b) + stage1_blocks = list(b.stage_blocks(1)) + self.assertEqual(len(stage1_blocks), 0) + self.assertEqual(b.get_block_stage(b), 0) + + b = TreeBlock(concrete=True) + b.setup(children_keys=[1, 2]) + b.children[1].setup(children_keys=list()) + b.children[2].setup(children_keys=['a', 'b']) + b.children[2].children['a'].setup(children_keys=list()) + b.children[2].children['b'].setup(children_keys=list()) + self.assertFalse(b.is_leaf()) + self.assertTrue(b.children[1].is_leaf()) + self.assertFalse(b.children[2].is_leaf()) + self.assertTrue(b.children[2].children['a'].is_leaf()) + self.assertTrue(b.children[2].children['b'].is_leaf()) + + with self.assertRaises(TreeBlockError): + b.x = pe.Var() + b.children[1].x = pe.Var() + with self.assertRaises(TreeBlockError): + b.children[2].x = pe.Var() + b.children[2].children['a'].x = pe.Var() + b.children[2].children['b'].x = pe.Var() + self.assertEqual(len(list(b.component_data_objects(pe.Var, descend_into=True, sort=True))), 3) + + self.assertEqual(b.num_stages(), 3) + with self.assertRaises(TreeBlockError): + b.children[1].num_stages() + with self.assertRaises(TreeBlockError): + b.children[2].num_stages() + with self.assertRaises(TreeBlockError): + b.children[2].children['a'].num_stages() + with self.assertRaises(TreeBlockError): + b.children[2].children['b'].num_stages() + + stage0_blocks = list(b.stage_blocks(0)) + stage1_blocks = list(b.stage_blocks(1)) + stage2_blocks = list(b.stage_blocks(2)) + stage3_blocks = list(b.stage_blocks(3)) + self.assertEqual(len(stage0_blocks), 1) + self.assertEqual(len(stage1_blocks), 2) + self.assertEqual(len(stage2_blocks), 2) + self.assertEqual(len(stage3_blocks), 0) + self.assertIs(stage0_blocks[0], b) + self.assertIs(stage1_blocks[0], b.children[1]) + self.assertIs(stage1_blocks[1], b.children[2]) + self.assertIs(stage2_blocks[0], b.children[2].children['a']) + self.assertIs(stage2_blocks[1], b.children[2].children['b']) + with self.assertRaises(TreeBlockError): + list(b.children[2].stage_blocks(0)) + b.children[1].deactivate() + stage1_blocks = list(b.stage_blocks(1, active=True)) + self.assertEqual(len(stage1_blocks), 1) + self.assertIs(stage1_blocks[0], b.children[2]) + stage1_blocks = list(b.stage_blocks(1)) + self.assertEqual(len(stage1_blocks), 2) + self.assertIs(stage1_blocks[0], b.children[1]) + self.assertIs(stage1_blocks[1], b.children[2]) + + self.assertEqual(b.get_block_stage(b), 0) + self.assertEqual(b.get_block_stage(b.children[1]), 1) + self.assertEqual(b.get_block_stage(b.children[2]), 1) + self.assertEqual(b.get_block_stage(b.children[2].children['a']), 2) + self.assertEqual(b.get_block_stage(b.children[2].children['b']), 2) + b.children[1].foo = pe.Block() + self.assertIs(b.get_block_stage(b.children[1].foo), None) + with self.assertRaises(TreeBlockError): + b.children[2].get_block_stage(b.children[2].children['a']) + + self.assertEqual(len(list(b.linking_constraints.values())), 0) + + +class TestGraphConversion(unittest.TestCase): + def test_convert_pyomo_model_to_bipartite_graph(self): + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.z = pe.Var() + m.c1 = pe.Constraint(expr=m.z >= m.x + m.y) + m.c2 = coramin.relaxations.PWXSquaredRelaxation() + m.c2.build(x=m.x, aux_var=m.z) + m.c3 = pe.Constraint(expr=m.z >= m.x - m.y) + + graph = convert_pyomo_model_to_bipartite_graph(m) + self.assertTrue(is_bipartite(graph)) + self.assertEqual(graph.number_of_nodes(), 6) + self.assertEqual(graph.number_of_edges(), 8) + graph_node_comps = ComponentSet([i.comp for i in graph.nodes()]) + self.assertEqual(len(graph_node_comps), 6) + self.assertIn(m.x, graph_node_comps) + self.assertIn(m.y, graph_node_comps) + self.assertIn(m.z, graph_node_comps) + self.assertIn(m.c1, graph_node_comps) + self.assertIn(m.c2, graph_node_comps) + self.assertIn(m.c3, graph_node_comps) + graph_edge_comps = {(id(i.comp), id(j.comp)) for i, j in graph.edges()} + self.assertTrue(((id(m.x), id(m.c1)) in graph_edge_comps) or ((id(m.c1), id(m.x)) in graph_edge_comps)) + self.assertTrue(((id(m.y), id(m.c1)) in graph_edge_comps) or ((id(m.c1), id(m.y)) in graph_edge_comps)) + self.assertTrue(((id(m.z), id(m.c1)) in graph_edge_comps) or ((id(m.c1), id(m.z)) in graph_edge_comps)) + self.assertTrue(((id(m.x), id(m.c2)) in graph_edge_comps) or ((id(m.c2), id(m.x)) in graph_edge_comps)) + self.assertFalse(((id(m.y), id(m.c2)) in graph_edge_comps) or ((id(m.c2), id(m.y)) in graph_edge_comps)) + self.assertTrue(((id(m.z), id(m.c2)) in graph_edge_comps) or ((id(m.c2), id(m.z)) in graph_edge_comps)) + self.assertTrue(((id(m.x), id(m.c3)) in graph_edge_comps) or ((id(m.c3), id(m.x)) in graph_edge_comps)) + self.assertTrue(((id(m.y), id(m.c3)) in graph_edge_comps) or ((id(m.c3), id(m.y)) in graph_edge_comps)) + self.assertTrue(((id(m.z), id(m.c3)) in graph_edge_comps) or ((id(m.c3), id(m.z)) in graph_edge_comps)) + self.assertEqual(num_cons_in_graph(graph=graph, include_rels=True), 3) + self.assertEqual(num_cons_in_graph(graph=graph, include_rels=False), 2) + + +class TestSplit(unittest.TestCase): + def setUp(self): + m = pe.ConcreteModel() + self.m = m + m.v1 = pe.Var() + m.v2 = pe.Var(bounds=(-1, 1)) + m.v3 = pe.Var(bounds=(-1, 1)) + m.v6 = pe.Var() + m.v4 = pe.Var(bounds=(-1, 1)) + m.v5 = pe.Var(bounds=(-1, 1)) + + m.c1 = pe.Constraint(expr=m.v1 - m.v2 - m.v3 == 0) + m.c2 = pe.Constraint(expr=m.v6 - m.v4 - m.v5 == 0) + m.r1 = coramin.relaxations.PWMcCormickRelaxation() + m.r1.set_input(x1=m.v4, x2=m.v5, aux_var=m.v6) + m.r2 = coramin.relaxations.PWMcCormickRelaxation() + m.r2.set_input(x1=m.v3, x2=m.v4, aux_var=m.v2) + + def test_split_metis(self): + m = self.m + + g = Graph() + v1 = _VarNode(m.v1) + v2 = _VarNode(m.v2) + v3 = _VarNode(m.v3) + v4 = _VarNode(m.v4) + v5 = _VarNode(m.v5) + v6 = _VarNode(m.v6) + c1 = _ConNode(m.c1) + c2 = _ConNode(m.c2) + r1 = _RelNode(m.r1) + r2 = _RelNode(m.r2) + + g.add_edge(v2, r2) + g.add_edge(v3, r2) + g.add_edge(v4, r2) + g.add_edge(v1, c1) + g.add_edge(v2, c1) + g.add_edge(v3, c1) + g.add_edge(v4, r1) + g.add_edge(v5, r1) + g.add_edge(v6, r1) + g.add_edge(v4, c2) + g.add_edge(v5, c2) + g.add_edge(v6, c2) + + tree, partitioning_ratio = split_metis(graph=g, model=m) + self.assertAlmostEqual(partitioning_ratio, 3*12/(14*1+6*2+6*2)) + + children = list(tree.children) + self.assertEqual(len(children), 2) + graph_a = children[0] + graph_b = children[1] + if v1 in graph_b.nodes(): + graph_a, graph_b = graph_b, graph_a + + graph_a_nodes = set(graph_a.nodes()) + graph_b_nodes = set(graph_b.nodes()) + self.assertIn(v1, graph_a_nodes) + self.assertIn(v2, graph_a_nodes) + self.assertIn(v3, graph_a_nodes) + self.assertIn(v4, graph_b_nodes) + self.assertIn(v5, graph_b_nodes) + self.assertIn(v6, graph_b_nodes) + self.assertIn(r2, graph_a_nodes) + self.assertIn(c1, graph_a_nodes) + self.assertIn(r1, graph_b_nodes) + self.assertIn(c2, graph_b_nodes) + self.assertEqual(len(graph_a_nodes), 6) + self.assertEqual(len(graph_b_nodes), 5) + v4_hat = list(graph_a_nodes - {v1, v2, v3, c1, r2})[0] + + graph_a_edges = set(graph_a.edges()) + graph_b_edges = set(graph_b.edges()) + self.assertTrue((v2, r2) in graph_a_edges or (r2, v2) in graph_a_edges) + self.assertTrue((v3, r2) in graph_a_edges or (r2, v3) in graph_a_edges) + self.assertTrue((v4_hat, r2) in graph_a_edges or (r2, v4_hat) in graph_a_edges) + self.assertTrue((v1, c1) in graph_a_edges or (c1, v1) in graph_a_edges) + self.assertTrue((v2, c1) in graph_a_edges or (c1, v2) in graph_a_edges) + self.assertTrue((v3, c1) in graph_a_edges or (c1, v3) in graph_a_edges) + self.assertTrue((v4, r1) in graph_b_edges or (r1, v4) in graph_b_edges) + self.assertTrue((v5, r1) in graph_b_edges or (r1, v5) in graph_b_edges) + self.assertTrue((v6, r1) in graph_b_edges or (r1, v6) in graph_b_edges) + self.assertTrue((v4, c2) in graph_b_edges or (c2, v4) in graph_b_edges) + self.assertTrue((v5, c2) in graph_b_edges or (c2, v5) in graph_b_edges) + self.assertTrue((v6, c2) in graph_b_edges or (c2, v6) in graph_b_edges) + self.assertEqual(len(graph_a_edges), 6) + self.assertEqual(len(graph_b_edges), 6) + + edges_between_children = list(tree.edges_between_children) + self.assertEqual(len(edges_between_children), 1) + edge = edges_between_children[0] + self.assertTrue((v4 is edge.node1 and v4_hat is edge.node2) or (v4 is edge.node2 and v4_hat is edge.node1)) + + new_model = TreeBlock(concrete=True) + component_map = tree.build_pyomo_model(block=new_model) + new_vars = list(coramin.relaxations.nonrelaxation_component_data_objects(new_model, + ctype=pe.Var, + descend_into=True, + sort=True)) + new_cons = list(coramin.relaxations.nonrelaxation_component_data_objects(new_model, + ctype=pe.Constraint, + active=True, + descend_into=True, + sort=True)) + new_rels = list(coramin.relaxations.relaxation_data_objects(new_model, + descend_into=True, + active=True, + sort=True)) + self.assertEqual(len(new_vars), 7) + self.assertEqual(len(new_cons), 3) + self.assertEqual(len(new_rels), 2) + self.assertEqual(len(new_model.children), 2) + self.assertEqual(len(new_model.linking_constraints), 1) + self.assertEqual(new_model.num_stages(), 2) + + stage0_vars = list(new_model.component_data_objects(pe.Var, descend_into=False, sort=True)) + stage0_cons = list(new_model.component_data_objects(pe.Constraint, descend_into=False, sort=True, active=True)) + stage0_rels = list(coramin.relaxations.relaxation_data_objects(new_model, + descend_into=False, + active=True, + sort=True)) + self.assertEqual(len(stage0_vars), 0) + self.assertEqual(len(stage0_cons), 1) + self.assertEqual(len(stage0_rels), 0) + + block_a = new_model.children[0] + block_b = new_model.children[1] + block_a_vars = ComponentSet(coramin.relaxations.nonrelaxation_component_data_objects(block_a, + ctype=pe.Var, + descend_into=True, + sort=True)) + block_b_vars = ComponentSet(coramin.relaxations.nonrelaxation_component_data_objects(block_b, + ctype=pe.Var, + descend_into=True, + sort=True)) + block_a_cons = ComponentSet(coramin.relaxations.nonrelaxation_component_data_objects(block_a, + ctype=pe.Constraint, + descend_into=True, + active=True, + sort=True)) + block_b_cons = ComponentSet(coramin.relaxations.nonrelaxation_component_data_objects(block_b, + ctype=pe.Constraint, + descend_into=True, + active=True, + sort=True)) + block_a_rels = ComponentSet(coramin.relaxations.relaxation_data_objects(block_a, + descend_into=True, + active=True, + sort=True)) + block_b_rels = ComponentSet(coramin.relaxations.relaxation_data_objects(block_b, + descend_into=True, + active=True, + sort=True)) + if component_map[m.v1] not in block_a_vars: + block_a, block_b = block_b, block_a + block_a_vars, block_b_vars = block_b_vars, block_a_vars + block_a_cons, block_b_cons = block_b_cons, block_a_cons + block_a_rels, block_b_rels = block_b_rels, block_a_rels + + self.assertEqual(len(block_a_vars), 4) + self.assertEqual(len(block_a_cons), 1) + self.assertEqual(len(block_a_rels), 1) + self.assertEqual(len(block_b_vars), 3) + self.assertEqual(len(block_b_cons), 1) + self.assertEqual(len(block_b_rels), 1) + + v1 = component_map[m.v1] + v2 = component_map[m.v2] + v3 = component_map[m.v3] + v4_a = block_a.vars['v4'] + v4_b = block_b.vars['v4'] + v5 = component_map[m.v5] + v6 = component_map[m.v6] + + self.assertIs(v1, block_a.vars['v1']) + self.assertIs(v2, block_a.vars['v2']) + self.assertIs(v3, block_a.vars['v3']) + self.assertIs(v5, block_b.vars['v5']) + self.assertIs(v6, block_b.vars['v6']) + + self.assertEqual(v2.lb, -1) + self.assertEqual(v2.ub, 1) + self.assertEqual(v3.lb, -1) + self.assertEqual(v3.ub, 1) + self.assertEqual(v4_a.lb, -1) + self.assertEqual(v4_a.ub, 1) + self.assertEqual(v4_b.lb, -1) + self.assertEqual(v4_b.ub, 1) + self.assertEqual(v5.lb, -1) + self.assertEqual(v5.ub, 1) + self.assertEqual(v1.lb, None) + self.assertEqual(v1.ub, None) + self.assertEqual(v6.lb, None) + self.assertEqual(v6.ub, None) + + linking_con = new_model.linking_constraints[1] + linking_con_vars = ComponentSet(identify_variables(linking_con.body)) + self.assertEqual(len(linking_con_vars), 2) + self.assertIn(v4_a, linking_con_vars) + self.assertIn(v4_b, linking_con_vars) + derivs = differentiate(expr=linking_con.body, mode=differentiate.Modes.reverse_symbolic) + self.assertTrue((derivs[v4_a] == 1 and derivs[v4_b] == -1) or + (derivs[v4_a] == -1 and derivs[v4_b] == 1)) + self.assertEqual(linking_con.lower, 0) + self.assertEqual(linking_con.upper, 0) + + c1 = block_a.cons['c1'] + c2 = block_b.cons['c2'] + r1 = block_b.rels.r1 + r2 = block_a.rels.r2 + c1_vars = ComponentSet(identify_variables(c1.body)) + c2_vars = ComponentSet(identify_variables(c2.body)) + self.assertEqual(len(c1_vars), 3) + self.assertEqual(len(c2_vars), 3) + self.assertIn(v1, c1_vars) + self.assertIn(v2, c1_vars) + self.assertIn(v3, c1_vars) + self.assertIn(v4_b, c2_vars) + self.assertIn(v5, c2_vars) + self.assertIn(v6, c2_vars) + self.assertIs(r1.get_aux_var(), v6) + self.assertIs(r2.get_aux_var(), v2) + r1_rhs_vars = ComponentSet(r1.get_rhs_vars()) + r2_rhs_vars = ComponentSet(r2.get_rhs_vars()) + self.assertIn(v3, r2_rhs_vars) + self.assertIn(v4_a, r2_rhs_vars) + self.assertIn(v4_b, r1_rhs_vars) + self.assertIn(v5, r1_rhs_vars) + self.assertTrue(isinstance(r1, coramin.relaxations.PWMcCormickRelaxationData)) + self.assertTrue(isinstance(r2, coramin.relaxations.PWMcCormickRelaxationData)) + c1_derivs = differentiate(c1.body, mode=differentiate.Modes.reverse_symbolic) + c2_derivs = differentiate(c2.body, mode=differentiate.Modes.reverse_symbolic) + self.assertEqual(c1_derivs[v1], 1) + self.assertEqual(c1_derivs[v2], -1) + self.assertEqual(c1_derivs[v3], -1) + self.assertEqual(c2_derivs[v4_b], -1) + self.assertEqual(c2_derivs[v5], -1) + self.assertEqual(c2_derivs[v6], 1) + self.assertEqual(c1.lower, 0) + self.assertEqual(c1.upper, 0) + self.assertEqual(c2.lower, 0) + self.assertEqual(c2.upper, 0) + + +class TestNumCons(unittest.TestCase): + def test_num_cons(self): + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(-1, 1)) + m.y = pe.Var() + m.z = pe.Var() + m.r = coramin.relaxations.PWUnivariateRelaxation() + m.r.build(x=m.x, + aux_var=m.y, + shape=coramin.utils.FunctionShape.CONVEX, + f_x_expr=pe.exp(m.x)) + m.c = pe.Constraint(expr=m.z == 2*m.x) + g = convert_pyomo_model_to_bipartite_graph(m) + self.assertEqual(num_cons_in_graph(g, include_rels=False), 1) + self.assertEqual(num_cons_in_graph(g), 2) + + +class TestDecompose(unittest.TestCase): + def helper(self, case, min_partition_ratio, expected_termination): + """ + we rely on other tests to make sure the relaxation is constructed + correctly. This test just checks the decomposition. + """ + + test_dir = os.path.dirname(os.path.abspath(__file__)) + pglib_dir = os.path.join(test_dir, 'pglib-opf-master') + if not os.path.isdir(pglib_dir): + get_pglib_opf(download_dir=test_dir) + md = ModelData.read(filename=os.path.join(pglib_dir, case)) + m, scaled_md = create_psv_acopf_model(md) + opt = pe.SolverFactory('ipopt') + res = opt.solve(m, tee=False) + + relaxed_m = coramin.relaxations.relax(m, + in_place=False, + use_fbbt=False, + fbbt_options={'deactivate_satisfied_constraints': True, + 'max_iter': 2}, + use_alpha_bb=False) + (decomposed_m, + component_map, + termination_reason) = decompose_model(model=relaxed_m, + max_leaf_nnz=1000, + min_partition_ratio=1.4, + limit_num_stages=True) + self.assertEqual(termination_reason, expected_termination) + if expected_termination == coramin.domain_reduction.dbt.DecompositionStatus.normal: + self.assertGreaterEqual(decomposed_m.num_stages(), 2) + + for r in coramin.relaxations.relaxation_data_objects(block=relaxed_m, descend_into=True, + active=True, sort=True): + r.rebuild(build_nonlinear_constraint=True) + for r in coramin.relaxations.relaxation_data_objects(block=decomposed_m, descend_into=True, + active=True, sort=True): + r.rebuild(build_nonlinear_constraint=True) + relaxed_res = opt.solve(relaxed_m, tee=False) + decomposed_res = opt.solve(decomposed_m, tee=False) + + self.assertEqual(res.solver.termination_condition, pe.TerminationCondition.optimal) + self.assertEqual(relaxed_res.solver.termination_condition, pe.TerminationCondition.optimal) + self.assertEqual(decomposed_res.solver.termination_condition, pe.TerminationCondition.optimal) + obj = get_objective(m) + relaxed_obj = get_objective(relaxed_m) + decomposed_obj = get_objective(decomposed_m) + val = pe.value(obj.expr) + relaxed_val = pe.value(relaxed_obj.expr) + decomposed_val = pe.value(decomposed_obj.expr) + relaxed_rel_diff = abs(val - relaxed_val) / val + decomposed_rel_diff = abs(val - decomposed_val) / val + self.assertAlmostEqual(relaxed_rel_diff, 0, 5) + self.assertAlmostEqual(decomposed_rel_diff, 0, 5) + + relaxed_vars = list(coramin.relaxations.nonrelaxation_component_data_objects(relaxed_m, + pe.Var, + sort=True, + descend_into=True)) + relaxed_vars = [v for v in relaxed_vars if not v.fixed] + relaxed_cons = list(coramin.relaxations.nonrelaxation_component_data_objects(relaxed_m, + pe.Constraint, + active=True, + sort=True, + descend_into=True)) + relaxed_rels = list(coramin.relaxations.relaxation_data_objects(relaxed_m, + descend_into=True, + active=True, + sort=True)) + decomposed_vars = list(coramin.relaxations.nonrelaxation_component_data_objects(decomposed_m, + pe.Var, + sort=True, + descend_into=True)) + decomposed_cons = list(coramin.relaxations.nonrelaxation_component_data_objects(decomposed_m, + pe.Constraint, + active=True, + sort=True, + descend_into=True)) + decomposed_rels = list(coramin.relaxations.relaxation_data_objects(decomposed_m, + descend_into=True, + active=True, + sort=True)) + linking_cons = list() + for stage in range(decomposed_m.num_stages()): + for block in decomposed_m.stage_blocks(stage): + if not block.is_leaf(): + linking_cons.extend(block.linking_constraints.values()) + relaxed_vars_mapped = list() + for i in relaxed_vars: + relaxed_vars_mapped.append(component_map[i]) + relaxed_vars_mapped = ComponentSet(relaxed_vars_mapped) + var_diff = ComponentSet(decomposed_vars) - relaxed_vars_mapped + extra_vars = ComponentSet() + for c in linking_cons: + for v in identify_variables(c.body, include_fixed=True): + extra_vars.add(v) + for v in coramin.relaxations.nonrelaxation_component_data_objects(decomposed_m, pe.Var, descend_into=True): + if 'dbt_partition_vars' in str(v) or 'obj_var' in str(v): + extra_vars.add(v) + extra_vars = extra_vars - relaxed_vars_mapped + partition_cons = ComponentSet() + obj_cons = ComponentSet() + for c in coramin.relaxations.nonrelaxation_component_data_objects(decomposed_m, pe.Constraint, active=True, descend_into=True): + if 'dbt_partition_cons' in str(c): + partition_cons.add(c) + elif 'obj_con' in str(c): + obj_cons.add(c) + for v in var_diff: + self.assertIn(v, extra_vars) + var_diff = relaxed_vars_mapped - ComponentSet(decomposed_vars) + self.assertEqual(len(var_diff), 0) + self.assertEqual(len(relaxed_vars) + len(extra_vars), len(decomposed_vars)) + + rcs = list() + for i in relaxed_cons + linking_cons + list(partition_cons) + list(obj_cons): + rcs.append(str(i)) + dcs = [str(i) for i in decomposed_cons] + + def _reformat(s: str) -> str: + s = s.split('.cons') + if len(s) > 1: + s = s[1] + s = s.lstrip('[') + s = s.rstrip(']') + elif s[0].startswith('cons'): + s = s[0] + s = s.lstrip('cons') + s = s.lstrip('[') + s = s.rstrip(']') + else: + s = s[0] + s = s.replace('"', '') + s = s.replace("'", "") + return s + + rcs = set([_reformat(i) for i in rcs]) + dcs = set([_reformat(i) for i in dcs]) + + self.assertEqual(rcs, dcs) + + # self.assertEqual(len(relaxed_cons) + len(linking_cons) + len(partition_cons) - len(partition_cons)/3 + len(obj_cons), len(decomposed_cons)) + self.assertEqual(len(relaxed_rels), len(decomposed_rels)) + + def test_decompose1(self): + self.helper('pglib_opf_case5_pjm.m', min_partition_ratio=1.5, expected_termination=coramin.domain_reduction.dbt.DecompositionStatus.problem_too_small) + + def test_decompose2(self): + self.helper('pglib_opf_case30_ieee.m', min_partition_ratio=1.5, expected_termination=coramin.domain_reduction.dbt.DecompositionStatus.normal) + + def test_decompose3(self): + self.helper('pglib_opf_case118_ieee.m', min_partition_ratio=1.5, expected_termination=coramin.domain_reduction.dbt.DecompositionStatus.normal) + + def test_decompose4(self): + self.helper('pglib_opf_case14_ieee.m', min_partition_ratio=1.4, expected_termination=coramin.domain_reduction.dbt.DecompositionStatus.normal) + + +class TestVarsToTightenByBlock(unittest.TestCase): + def test_vars_to_tighten_by_block(self): + m = TreeBlock(concrete=True) + m.setup(children_keys=[1, 2]) + b1 = m.children[1] + b2 = m.children[2] + b1.setup(children_keys=list()) + b2.setup(children_keys=list()) + + b1.x = pe.Var(bounds=(-1, 1)) + b1.y = pe.Var() + b1.z = pe.Var() + b1.aux = pe.Var() + + b2.x = pe.Var(bounds=(-1, 1)) + b2.y = pe.Var() + b2.z = pe.Var() + b2.aux = pe.Var() + + b1.c = pe.Constraint(expr=b1.x + b1.y + b1.z == 0) + b2.c = pe.Constraint(expr=b2.x + b2.y + b2.z == 0) + + b1.r = coramin.relaxations.PWUnivariateRelaxation() + b1.r.set_input(x=b1.x, + aux_var=b1.aux, + shape=coramin.utils.FunctionShape.CONVEX, + f_x_expr=pe.exp(b1.x)) + b1.r.rebuild() + + b2.r = coramin.relaxations.PWXSquaredRelaxation() + b2.r.set_input(x=b2.x, + aux_var=b2.aux, + relaxation_side=coramin.utils.RelaxationSide.UNDER) + b2.r.rebuild() + + m.linking_constraints.add(b1.z == b2.z) + + vars_to_tighten_by_block = collect_vars_to_tighten_by_block(m, method='full_space') + self.assertEqual(len(vars_to_tighten_by_block), 3) + vars_to_tighten = vars_to_tighten_by_block[m] + self.assertEqual(len(vars_to_tighten), 0) + vars_to_tighten = vars_to_tighten_by_block[b1] + self.assertEqual(len(vars_to_tighten), 1) + self.assertIn(b1.x, vars_to_tighten) + vars_to_tighten = vars_to_tighten_by_block[b2] + self.assertEqual(len(vars_to_tighten), 1) + self.assertIn(b2.x, vars_to_tighten) + + vars_to_tighten_by_block = collect_vars_to_tighten_by_block(m, method='leaves') + self.assertEqual(len(vars_to_tighten_by_block), 3) + vars_to_tighten = vars_to_tighten_by_block[m] + self.assertEqual(len(vars_to_tighten), 0) + vars_to_tighten = vars_to_tighten_by_block[b1] + self.assertEqual(len(vars_to_tighten), 1) + self.assertIn(b1.x, vars_to_tighten) + vars_to_tighten = vars_to_tighten_by_block[b2] + self.assertEqual(len(vars_to_tighten), 1) + self.assertIn(b2.x, vars_to_tighten) + + vars_to_tighten_by_block = collect_vars_to_tighten_by_block(m, method='dbt') + self.assertEqual(len(vars_to_tighten_by_block), 3) + vars_to_tighten = vars_to_tighten_by_block[m] + self.assertEqual(len(vars_to_tighten), 1) + self.assertTrue(b1.z in vars_to_tighten or b2.z in vars_to_tighten) + vars_to_tighten = vars_to_tighten_by_block[b1] + self.assertEqual(len(vars_to_tighten), 1) + self.assertIn(b1.x, vars_to_tighten) + vars_to_tighten = vars_to_tighten_by_block[b2] + self.assertEqual(len(vars_to_tighten), 1) + self.assertIn(b2.x, vars_to_tighten) + + +class TestDBT(unittest.TestCase): + def get_model(self): + m = TreeBlock(concrete=True) + m.setup(children_keys=[0, 1]) + b0 = m.children[0] + b1 = m.children[1] + b0.setup(children_keys=list()) + b1.setup(children_keys=list()) + + b0.x = pe.Var(bounds=(-1, 1)) + b0.y = pe.Var(bounds=(-5, 5)) + b0.p = pe.Param(initialize=1.0, mutable=True) + b0.c = coramin.relaxations.PWUnivariateRelaxation() + b0.c.build(x=b0.x, aux_var=b0.y, shape=coramin.utils.FunctionShape.CONVEX, f_x_expr=b0.p*b0.x) + + b1.x = pe.Var(bounds=(-5, 5)) + b1.y = pe.Var(bounds=(-5, 5)) + b1.p = pe.Param(initialize=1.0, mutable=True) + b1.c = coramin.relaxations.PWUnivariateRelaxation() + b1.c.build(x=b1.x, aux_var=b1.y, shape=coramin.utils.FunctionShape.CONVEX, f_x_expr=b1.p*b1.x) + + m.linking_constraints.add(b0.y == b1.y) + + return m + + def test_full_space(self): + m = self.get_model() + b0 = m.children[0] + b1 = m.children[1] + opt = appsi.solvers.Gurobi() + perform_dbt(relaxation=m, solver=opt, obbt_method=OBBTMethod.FULL_SPACE, filter_method=FilterMethod.NONE) + self.assertAlmostEqual(b0.x.lb, -1) + self.assertAlmostEqual(b0.x.ub, 1) + self.assertAlmostEqual(b0.y.lb, -5) + self.assertAlmostEqual(b0.y.ub, 5) + self.assertAlmostEqual(b1.x.lb, -1) + self.assertAlmostEqual(b1.x.ub, 1) + self.assertAlmostEqual(b1.y.lb, -5) + self.assertAlmostEqual(b1.y.ub, 5) + + def test_leaves(self): + m = self.get_model() + b0 = m.children[0] + b1 = m.children[1] + opt = appsi.solvers.Gurobi() + perform_dbt(relaxation=m, solver=opt, obbt_method=OBBTMethod.LEAVES, filter_method=FilterMethod.NONE) + self.assertAlmostEqual(b0.x.lb, -1) + self.assertAlmostEqual(b0.x.ub, 1) + self.assertAlmostEqual(b0.y.lb, -5) + self.assertAlmostEqual(b0.y.ub, 5) + self.assertAlmostEqual(b1.x.lb, -5) + self.assertAlmostEqual(b1.x.ub, 5) + self.assertAlmostEqual(b1.y.lb, -5) + self.assertAlmostEqual(b1.y.ub, 5) + + def test_dbt(self): + m = self.get_model() + b0 = m.children[0] + b1 = m.children[1] + opt = appsi.solvers.Gurobi() + perform_dbt(relaxation=m, solver=opt, obbt_method=OBBTMethod.DECOMPOSED, filter_method=FilterMethod.NONE) + self.assertAlmostEqual(b0.x.lb, -1) + self.assertAlmostEqual(b0.x.ub, 1) + self.assertAlmostEqual(b0.y.lb, -1) + self.assertAlmostEqual(b0.y.ub, 1) + self.assertAlmostEqual(b1.x.lb, -1) + self.assertAlmostEqual(b1.x.ub, 1) + self.assertAlmostEqual(b1.y.lb, -1) + self.assertAlmostEqual(b1.y.ub, 1) + + def test_dbt_with_filter(self): + m = self.get_model() + b0 = m.children[0] + b1 = m.children[1] + opt = appsi.solvers.Gurobi() + perform_dbt(relaxation=m, solver=opt, obbt_method=OBBTMethod.DECOMPOSED, filter_method=FilterMethod.AGGRESSIVE) + self.assertAlmostEqual(b0.x.lb, -1) + self.assertAlmostEqual(b0.x.ub, 1) + self.assertAlmostEqual(b0.y.lb, -1) + self.assertAlmostEqual(b0.y.ub, 1) + self.assertAlmostEqual(b1.x.lb, -1) + self.assertAlmostEqual(b1.x.ub, 1) + self.assertAlmostEqual(b1.y.lb, -1) + self.assertAlmostEqual(b1.y.ub, 1) + + +class TestDBTWithECP(unittest.TestCase): + def create_model(self): + m = coramin.domain_reduction.TreeBlock(concrete=True) + m.setup(children_keys=[1, 2]) + m.children[1].setup(children_keys=[1, 2]) + m.children[2].setup(children_keys=[1, 2]) + m.children[1].children[1].setup(children_keys=list()) + m.children[1].children[2].setup(children_keys=list()) + m.children[2].children[1].setup(children_keys=list()) + m.children[2].children[2].setup(children_keys=list()) + + b1 = m.children[1].children[1] + b2 = m.children[1].children[2] + b3 = m.children[2].children[1] + b4 = m.children[2].children[2] + + b1.x1 = pe.Var(bounds=(0.5, 5)) + b1.x2 = pe.Var(bounds=(0.5, 5)) + b1.x3 = pe.Var(bounds=(0.5, 5)) + + b2.x4 = pe.Var(bounds=(0.5, 5)) + b2.x5 = pe.Var(bounds=(0.5, 5)) + b2.x6 = pe.Var(bounds=(0.5, 5)) + + b3.x7 = pe.Var(bounds=(0.5, 5)) + b3.x8 = pe.Var(bounds=(0.5, 5)) + b3.x9 = pe.Var(bounds=(0.5, 5)) + + b4.x10 = pe.Var(bounds=(0.5, 5)) + b4.x11 = pe.Var(bounds=(0.5, 5)) + b4.x12 = pe.Var(bounds=(0.5, 5)) + + b1.c1 = pe.Constraint(expr=b1.x1 == b1.x2 ** 2 - b1.x3 ** 2) + b1.c2 = pe.Constraint(expr=b1.x2 == pe.log(b1.x3) + b1.x3) + + b2.c1 = pe.Constraint(expr=b2.x4 == b2.x5 * b2.x6) + b2.c2 = pe.Constraint(expr=b2.x5 == b2.x6 ** 2) + + b3.c1 = pe.Constraint(expr=b3.x7 == pe.log(b3.x8) - pe.log(b3.x9)) + b3.c2 = pe.Constraint(expr=b3.x8 + b3.x9 == 4) + + b4.c1 = pe.Constraint(expr=b4.x10 == b4.x11 * b4.x12 - b4.x12) + b4.c2 = pe.Constraint(expr=b4.x11 + b4.x12 == 4) + + m.children[1].linking_constraints.add(b1.x3 == b2.x6) + m.children[2].linking_constraints.add(b3.x9 == b4.x10) + m.linking_constraints.add(b1.x3 == b3.x9) + + m.obj = pe.Objective( + expr=b1.x1 + b1.x2 + b1.x3 + b2.x4 + b2.x5 + b2.x6 + b3.x7 + b3.x8 + b3.x9 + b4.x10 + b4.x11 + b4.x12) + + return m + + @pytest.mark.parallel + @pytest.mark.two_proc + @pytest.mark.three_proc + def test_bounds_tightening(self): + from mpi4py import MPI + + comm: MPI.Comm = MPI.COMM_WORLD + rank = comm.Get_rank() + + m = self.create_model() + coramin.relaxations.relax(m, descend_into=True, in_place=True) + opt = coramin.algorithms.ECPBounder(subproblem_solver=appsi.solvers.Gurobi()) + opt.config.keep_cuts = False + opt.config.feasibility_tol = 1e-5 + coramin.domain_reduction.perform_dbt(m, opt, filter_method=coramin.domain_reduction.FilterMethod.NONE, + parallel=True) + m.write(f'rank{rank}.lp') + comm.Barrier() + if rank == 0: + self.assertTrue(filecmp.cmp('rank1.lp', f'rank{rank}.lp'), f'rank {rank}') + else: + self.assertTrue(filecmp.cmp('rank0.lp', f'rank{rank}.lp'), f'rank {rank}') + + # the next bit of code is needed to ensure the above test actually tests what we think it is testing + m = self.create_model() + coramin.relaxations.relax(m, descend_into=True, in_place=True) + opt = coramin.algorithms.ECPBounder(subproblem_solver=appsi.solvers.Gurobi()) + opt.config.keep_cuts = False + opt.config.feasibility_tol = 1e-5 + coramin.domain_reduction.perform_dbt(m, opt, filter_method=coramin.domain_reduction.FilterMethod.NONE, + parallel=True, update_relaxations_between_stages=False) + m.write(f'rank{rank}.lp') + comm.Barrier() + if rank == 0: + self.assertFalse(filecmp.cmp('rank1.lp', f'rank{rank}.lp')) + else: + self.assertFalse(filecmp.cmp('rank0.lp', f'rank{rank}.lp')) diff --git a/pyomo/contrib/coramin/domain_reduction/tests/test_filters.py b/pyomo/contrib/coramin/domain_reduction/tests/test_filters.py new file mode 100644 index 00000000000..e15dd10be98 --- /dev/null +++ b/pyomo/contrib/coramin/domain_reduction/tests/test_filters.py @@ -0,0 +1,32 @@ +import pyomo.environ as pe +import coramin +import unittest +from pyomo.contrib import appsi + + +class TestFilters(unittest.TestCase): + def test_basic_filter(self): + m = pe.ConcreteModel() + m.y = pe.Var() + m.x = pe.Var(bounds=(-2, -1)) + m.obj = pe.Objective(expr=m.y) + m.c = pe.Constraint(expr=m.y == -m.x**2) + coramin.relaxations.relax(m, in_place=True) + opt = appsi.solvers.Gurobi() + res = opt.solve(m) + vars_to_min, vars_to_max = coramin.domain_reduction.filter_variables_from_solution([m.x]) + self.assertIn(m.x, vars_to_max) + self.assertNotIn(m.x, vars_to_min) + + def test_aggressive_filter(self): + m = pe.ConcreteModel() + m.y = pe.Var() + m.x = pe.Var(bounds=(-2, -1)) + m.obj = pe.Objective(expr=m.y) + m.c = pe.Constraint(expr=m.y == -m.x**2) + coramin.relaxations.relax(m, in_place=True) + opt = appsi.solvers.Gurobi() + vars_to_min, vars_to_max = coramin.domain_reduction.aggressive_filter(candidate_variables=[m.x], relaxation=m, + solver=opt) + self.assertNotIn(m.x, vars_to_max) + self.assertNotIn(m.x, vars_to_min) diff --git a/pyomo/contrib/coramin/domain_reduction/tests/test_obbt.py b/pyomo/contrib/coramin/domain_reduction/tests/test_obbt.py new file mode 100644 index 00000000000..8c0a151dc85 --- /dev/null +++ b/pyomo/contrib/coramin/domain_reduction/tests/test_obbt.py @@ -0,0 +1,113 @@ +import coramin +import unittest +import pyomo.environ as pyo +from pyomo.contrib import appsi + + +class TestBoundsTightener(unittest.TestCase): + @classmethod + def setUpClass(cls): + pass + + @classmethod + def tearDownClass(cls): + pass + + def test_quad(self): + model = pyo.ConcreteModel() + model.x = pyo.Var(bounds=(-5.0, 5.0)) + model.y = pyo.Var(bounds=(-100.0, 100.0)) + + model.obj_expr = pyo.Expression(expr=model.y) + model.obj = pyo.Objective(expr=model.obj_expr) + + x_points = [-5.0, 5.0] + model.under_estimators = pyo.ConstraintList() + for xp in x_points: + m = 2*xp + b = -(xp**2) + model.under_estimators.add(model.y >= m*model.x + b) + + solver = appsi.solvers.Ipopt() + (lower, upper) = coramin.domain_reduction.perform_obbt(model=model, solver=solver, varlist=[model.x, model.y], + update_bounds=True) + self.assertAlmostEqual(pyo.value(model.x.lb), -5.0, delta=1e-6) + self.assertAlmostEqual(pyo.value(model.x.ub), 5.0, delta=1e-6) + self.assertAlmostEqual(pyo.value(model.y.lb), -25.0, delta=1e-6) + self.assertAlmostEqual(pyo.value(model.y.ub), 100.0, delta=1e-6) + self.assertAlmostEqual(lower[0], -5.0, delta=1e-6) + self.assertAlmostEqual(upper[0], 5.0, delta=1e-6) + self.assertAlmostEqual(lower[1], -25.0, delta=1e-6) + self.assertAlmostEqual(upper[1], 100.0, delta=1e-6) + + def test_passing_component_not_list(self): + model = pyo.ConcreteModel() + model.x = pyo.Var(bounds=(-5.0, 5.0)) + model.y = pyo.Var(bounds=(-100.0, 100.0)) + + model.obj_expr = pyo.Expression(expr=model.y) + model.obj = pyo.Objective(expr=model.obj_expr) + + x_points = [-5.0, 5.0] + model.under_estimators = pyo.ConstraintList() + for xp in x_points: + m = 2 * xp + b = -(xp ** 2) + model.under_estimators.add(model.y >= m * model.x + b) + + solver = appsi.solvers.Ipopt() + (lower, upper) = coramin.domain_reduction.perform_obbt(model=model, solver=solver, varlist=model.y, update_bounds=True) + self.assertAlmostEqual(pyo.value(model.x.lb), -5.0, delta=1e-6) + self.assertAlmostEqual(pyo.value(model.x.ub), 5.0, delta=1e-6) + self.assertAlmostEqual(pyo.value(model.y.lb), -25.0, delta=1e-6) + self.assertAlmostEqual(pyo.value(model.y.ub), 100.0, delta=1e-6) + self.assertAlmostEqual(lower[0], -25.0, delta=1e-6) + self.assertAlmostEqual(upper[0], 100.0, delta=1e-6) + + def test_passing_indexed_component_not_list(self): + model = pyo.ConcreteModel() + model.x = pyo.Var(bounds=(-5.0, 5.0)) + model.S = pyo.Set(initialize=['A', 'B'], ordered=True) + model.y = pyo.Var(model.S, bounds=(-100.0, 100.0)) + + model.obj_expr = pyo.Expression(expr=model.y['A']) + model.obj = pyo.Objective(expr=model.obj_expr) + + x_points = [-5.0, 5.0] + model.under_estimators = pyo.ConstraintList() + for xp in x_points: + m = 2 * xp + b = -(xp ** 2) + model.under_estimators.add(model.y['A'] >= m * model.x + b) + + model.con = pyo.Constraint(expr=model.y['A'] == 1 + model.y['B']) + + solver = appsi.solvers.Ipopt() + lower, upper = coramin.domain_reduction.perform_obbt(model=model, solver=solver, varlist=model.y, update_bounds=True) + self.assertAlmostEqual(pyo.value(model.x.lb), -5.0, delta=1e-6) + self.assertAlmostEqual(pyo.value(model.x.ub), 5.0, delta=1e-6) + self.assertAlmostEqual(pyo.value(model.y['A'].lb), -25.0, delta=1e-6) + self.assertAlmostEqual(pyo.value(model.y['A'].ub), 100.0, delta=1e-6) + self.assertAlmostEqual(pyo.value(model.y['B'].lb), -26.0, delta=1e-6) + self.assertAlmostEqual(pyo.value(model.y['B'].ub), 99.0, delta=1e-6) + self.assertAlmostEqual(lower[0], -25.0, delta=1e-6) + self.assertAlmostEqual(upper[0], 100.0, delta=1e-6) + self.assertAlmostEqual(lower[1], -26.0, delta=1e-6) + self.assertAlmostEqual(upper[1], 99.0, delta=1e-6) + + def test_too_many_obj(self): + model = pyo.ConcreteModel() + model.x = pyo.Var(bounds=(-5.0, 5.0)) + model.y = pyo.Var(bounds=(-100.0, 100.0)) + + model.obj1 = pyo.Objective(expr=model.x + model.y) + model.obj2 = pyo.Objective(expr=model.x - model.y) + + solver = pyo.SolverFactory('ipopt') + with self.assertRaises(ValueError): + coramin.domain_reduction.perform_obbt(model=model, solver=solver, varlist=[model.x, model.y], + objective_bound=0.0, update_bounds=True) + + +if __name__ == '__main__': + TestBoundsTightener() diff --git a/pyomo/contrib/coramin/examples/alpha_bb.py b/pyomo/contrib/coramin/examples/alpha_bb.py new file mode 100644 index 00000000000..af4d6c36ef3 --- /dev/null +++ b/pyomo/contrib/coramin/examples/alpha_bb.py @@ -0,0 +1,50 @@ +import pyomo.environ as pe +import coramin +from coramin.utils.plot_relaxation import plot_relaxation +from pyomo.contrib import appsi + + +def main(): + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(0.001, 2)) + m.y = pe.Var(bounds=(0.01, 10)) + m.z = pe.Var() + m.c = coramin.relaxations.AlphaBBRelaxation() + + m.c.build( + aux_var=m.z, + f_x_expr=m.x*pe.log(m.x/m.y), + relaxation_side=coramin.RelaxationSide.UNDER, + eigenvalue_bounder=coramin.EigenValueBounder.GershgorinWithSimplification, + ) + m.x.value = m.x.lb + m.y.value = m.y.ub + m.c.add_cut(keep_cut=True, check_violation=False) + m.x.value = m.x.ub + m.y.value = m.y.lb + m.c.add_cut(keep_cut=True, check_violation=False) + + opt = pe.SolverFactory('gurobi_persistent') + opt.set_instance(m) + plot_relaxation(m, m.c, opt) + + m.c.hessian.method = coramin.EigenValueBounder.LinearProgram + m.c.hessian.opt = appsi.solvers.Gurobi() + m.c.rebuild() + plot_relaxation(m, m.c, opt) + + m.c.hessian.method = coramin.EigenValueBounder.Global + mip_opt = appsi.solvers.Gurobi() + nlp_opt = appsi.solvers.Ipopt() + eigenvalue_opt = coramin.algorithms.MultiTree( + mip_solver=mip_opt, + nlp_solver=nlp_opt, + ) + eigenvalue_opt.config.convexity_effort = 'medium' + m.c.hessian.opt = eigenvalue_opt + m.c.rebuild() + plot_relaxation(m, m.c, opt) + + +if __name__ == '__main__': + main() diff --git a/pyomo/contrib/coramin/examples/dbt.py b/pyomo/contrib/coramin/examples/dbt.py new file mode 100644 index 00000000000..94146ad66eb --- /dev/null +++ b/pyomo/contrib/coramin/examples/dbt.py @@ -0,0 +1,90 @@ +""" +This example demonstrates how to used decomposed bounds +tightening. The example problem is an ACOPF problem. +""" +import pyomo.environ as pe +import coramin +from egret.data.model_data import ModelData +from egret.thirdparty.get_pglib_opf import get_pglib_opf +from egret.models.ac_relaxations import create_polar_acopf_relaxation +from egret.models.acopf import create_psv_acopf_model +import itertools +import os +import time + + +# Create the NLP and the relaxation +print('Downloading Power Grid Lib') +if not os.path.exists('pglib-opf-master'): + get_pglib_opf() + +print('Creating NLP and relaxation') +md = ModelData.read('pglib-opf-master/api/pglib_opf_case73_ieee_rts__api.m') +nlp, scaled_md = create_psv_acopf_model(md) +relaxation, scaled_md2 = create_polar_acopf_relaxation(md) + +# perform decomposition +print('Decomposing relaxation') +relaxation, component_map, termination_reason = coramin.domain_reduction.decompose_model(relaxation, max_leaf_nnz=1000) + +# Add more outer approximation points for the second order cone constraints +print('Adding extra outer-approximation points for SOC constraints') +for b in coramin.relaxations.relaxation_data_objects(relaxation, descend_into=True, active=True, sort=True): + if isinstance(b, coramin.relaxations.MultivariateRelaxationData): + b.clear_oa_points() + for bnd_combination in itertools.product(*[itertools.product(['L', 'U'], [v]) for v in b.get_rhs_vars()]): + bnd_dict = pe.ComponentMap() + for lower_or_upper, v in bnd_combination: + if lower_or_upper == 'L': + if v.has_lb(): + bnd_dict[v] = v.lb + else: + bnd_dict[v] = -1 + else: + assert lower_or_upper == 'U' + if v.has_ub(): + bnd_dict[v] = v.ub + else: + bnd_dict[v] = 1 + b.add_oa_point(var_values=bnd_dict) + +# rebuild the relaxations +for b in coramin.relaxations.relaxation_data_objects(relaxation, descend_into=True, active=True, sort=True): + b.rebuild() + +# create solvers +nlp_opt = pe.SolverFactory('ipopt') +rel_opt = pe.SolverFactory('gurobi_persistent') + +# solve the nlp to get the upper bound +print('Solving NLP') +res = nlp_opt.solve(nlp) +assert res.solver.termination_condition == pe.TerminationCondition.optimal +ub = pe.value(coramin.utils.get_objective(nlp)) + +# solve the relaxation to get the lower bound +print('Solving relaxation') +rel_opt.set_instance(relaxation) +res = rel_opt.solve(save_results=False) +assert res.solver.termination_condition == pe.TerminationCondition.optimal +lb = pe.value(coramin.utils.get_objective(relaxation)) +gap = (ub - lb) / ub * 100 +print('{ub:<20}{lb:<20}{gap:<20}{time:<20}'.format(ub='UB', lb='LB', gap='% gap', time='Time')) +t0 = time.time() +print('{ub:<20.2f}{lb:<20.2f}{gap:<20.2f}{time:<20.2f}'.format(ub=ub, lb=lb, gap=gap, time=time.time() - t0)) + +for _iter in range(3): + coramin.domain_reduction.perform_dbt(relaxation=relaxation, + solver=rel_opt, + obbt_method=coramin.domain_reduction.OBBTMethod.DECOMPOSED, + filter_method=coramin.domain_reduction.FilterMethod.AGGRESSIVE, + objective_bound=ub, + with_progress_bar=True) + for r in coramin.relaxations.relaxation_data_objects(relaxation, descend_into=True, active=True, sort=True): + r.rebuild() + rel_opt.set_instance(relaxation) + res = rel_opt.solve(save_results=False) + assert res.solver.termination_condition == pe.TerminationCondition.optimal + lb = pe.value(coramin.utils.get_objective(relaxation)) + gap = (ub - lb) / ub * 100 + print('{ub:<20.2f}{lb:<20.2f}{gap:<20.2f}{time:<20.2f}'.format(ub=ub, lb=lb, gap=gap, time=time.time() - t0)) diff --git a/pyomo/contrib/coramin/examples/dbt2.py b/pyomo/contrib/coramin/examples/dbt2.py new file mode 100644 index 00000000000..8864997b5e7 --- /dev/null +++ b/pyomo/contrib/coramin/examples/dbt2.py @@ -0,0 +1,80 @@ +""" +This example demonstrates how to used decomposed bounds +tightening. The example problem is from minlplib. In order to run +this example, you have to download the problem file corresponding to +the filename in the "read_osil" function bedlow. The file can be +downloaded from minlplib.org. Suspect is also needed. +""" +import pyomo.environ as pe +import coramin +import itertools +import os +import time +from suspect.pyomo import read_osil +from coramin.third_party.minlplib_tools import get_minlplib + + +print('Downloading camshape800 from MINLPLib') +if not os.path.exists(os.path.join('minlplib', 'osil', 'camshape800.osil')): + get_minlplib(problem_name='camshape800') + +print('Creating NLP and relaxation') +nlp = read_osil('minlplib/osil/camshape800.osil', objective_prefix='obj_', constraint_prefix='con_') +relaxation = coramin.relaxations.relax(nlp, in_place=False, use_fbbt=True, fbbt_options={'deactivate_satisfied_constraints': True, + 'max_iter': 2}) + +# perform decomposition +print('Decomposing relaxation') +relaxation, component_map, termination_reason = coramin.domain_reduction.decompose_model(relaxation, max_leaf_nnz=1000) + +# rebuild the relaxations +for b in coramin.relaxations.relaxation_data_objects(relaxation, descend_into=True, active=True, sort=True): + b.rebuild() + +# create solvers +nlp_opt = pe.SolverFactory('ipopt') +rel_opt = pe.SolverFactory('gurobi_persistent') + +# solve the nlp to get the upper bound +print('Solving NLP') +res = nlp_opt.solve(nlp) +assert res.solver.termination_condition == pe.TerminationCondition.optimal +ub = pe.value(coramin.utils.get_objective(nlp)) + +# solve the relaxation to get the lower bound +print('Solving relaxation') +rel_opt.set_instance(relaxation) +res = rel_opt.solve(save_results=False) +assert res.solver.termination_condition == pe.TerminationCondition.optimal +lb = pe.value(coramin.utils.get_objective(relaxation)) +gap = (ub - lb) / abs(ub) * 100 +var_bounds = pe.ComponentMap() +for r in coramin.relaxations.relaxation_data_objects(relaxation, descend_into=True, active=True, sort=True): + for v in r.get_rhs_vars(): + var_bounds[v] = v.ub - v.lb +avg_bound_range = sum(var_bounds.values()) / len(var_bounds) +print('{ub:<20}{lb:<20}{gap:<20}{avg_rng:<20}{time:<20}'.format(ub='UB', lb='LB', gap='% gap', avg_rng='Avg Var Range', time='Time')) +t0 = time.time() +print('{ub:<20.3f}{lb:<20.3f}{gap:<20.3f}{avg_rng:<20.3e}{time:<20.3f}'.format(ub=ub, lb=lb, gap=gap, avg_rng=avg_bound_range, time=time.time() - t0)) + +# Perform bounds tightening +for _iter in range(3): + coramin.domain_reduction.perform_dbt(relaxation=relaxation, + solver=rel_opt, + obbt_method=coramin.domain_reduction.OBBTMethod.DECOMPOSED, + filter_method=coramin.domain_reduction.FilterMethod.AGGRESSIVE, + objective_bound=ub, + with_progress_bar=True) + for r in coramin.relaxations.relaxation_data_objects(relaxation, descend_into=True, active=True, sort=True): + r.rebuild() + rel_opt.set_instance(relaxation) + res = rel_opt.solve(save_results=False) + assert res.solver.termination_condition == pe.TerminationCondition.optimal + lb = pe.value(coramin.utils.get_objective(relaxation)) + gap = (ub - lb) / abs(ub) * 100 + var_bounds = pe.ComponentMap() + for r in coramin.relaxations.relaxation_data_objects(relaxation, descend_into=True, active=True, sort=True): + for v in r.get_rhs_vars(): + var_bounds[v] = v.ub - v.lb + avg_bound_range = sum(var_bounds.values()) / len(var_bounds) + print('{ub:<20.3f}{lb:<20.3f}{gap:<20.3f}{avg_rng:<20.3e}{time:<20.3f}'.format(ub=ub, lb=lb, gap=gap, avg_rng=avg_bound_range, time=time.time() - t0)) diff --git a/pyomo/contrib/coramin/examples/ex.py b/pyomo/contrib/coramin/examples/ex.py new file mode 100644 index 00000000000..4ec272f34c7 --- /dev/null +++ b/pyomo/contrib/coramin/examples/ex.py @@ -0,0 +1,86 @@ +import pyomo.environ as pe +import coramin +from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr + + +""" +This example demonstrates a couple features of Coramin: +- Using the "add_cut" methods to refine relaxations with linear constraints +- Optimization-based bounds tightening + +The example problem is + +min x**4 - 3*x**2 + x +""" + + +# Build and solve the NLP +nlp = pe.ConcreteModel() +nlp.x = pe.Var(bounds=(-2, 2)) +nlp.obj = pe.Objective(expr=nlp.x**4 - 3*nlp.x**2 + nlp.x) +opt = pe.SolverFactory('ipopt') +res = opt.solve(nlp) +ub = pe.value(nlp.obj) + +# Build the relaxation +""" +Reformulate the NLP as + +min x4 - 3*x2 + x +s.t. + x2 = x**2 + x4 = x2**2 + +Then relax the two constraints with PWXSquaredRelaxation objects. +""" +rel = pe.ConcreteModel() +rel.x = pe.Var(bounds=(-2, 2)) +rel.x2 = pe.Var(bounds=compute_bounds_on_expr(rel.x**2)) +rel.x4 = pe.Var(bounds=compute_bounds_on_expr(rel.x2**2)) +rel.x2_con = coramin.relaxations.PWXSquaredRelaxation() +rel.x2_con.build(x=rel.x, aux_var=rel.x2, use_linear_relaxation=True) +rel.x4_con = coramin.relaxations.PWXSquaredRelaxation() +rel.x4_con.build(x=rel.x2, aux_var=rel.x4, use_linear_relaxation=True) +rel.obj = pe.Objective(expr=rel.x4 - 3*rel.x2 + rel.x) + + +# Now solve the relaxation and refine the convex sides of the constraints with add_cut +print('*********************************') +print('OA Cut Generation') +print('*********************************') +opt = pe.SolverFactory('gurobi_direct') +res = opt.solve(rel) +lb = pe.value(rel.obj) +print('gap: ' + str(100 * abs(ub - lb) / abs(ub)) + ' %') + +for _iter in range(10): + for b in rel.component_data_objects(pe.Block, active=True, sort=True, descend_into=True): + if isinstance(b, coramin.relaxations.BaseRelaxationData): + b.add_cut() + res = opt.solve(rel) + lb = pe.value(rel.obj) + print('gap: ' + str(100 * abs(ub - lb) / abs(ub)) + ' %') + +# we want to discard the cuts generated above just to demonstrate OBBT +for b in rel.component_data_objects(pe.Block, active=True, sort=True, descend_into=True): + if isinstance(b, coramin.relaxations.BasePWRelaxationData): + b.clear_oa_points() + b.rebuild() + +# Now refine the relaxation with OBBT +print('\n*********************************') +print('OBBT') +print('*********************************') +res = opt.solve(rel) +lb = pe.value(rel.obj) +print('gap: ' + str(100 * abs(ub - lb) / abs(ub)) + ' %') +for _iter in range(10): + coramin.domain_reduction.perform_obbt(rel, opt, [rel.x, rel.x2], objective_bound=ub) + for b in rel.component_data_objects(pe.Block, active=True, sort=True, descend_into=True): + if isinstance(b, coramin.relaxations.BasePWRelaxationData): + b.rebuild() + res = opt.solve(rel) + lb = pe.value(rel.obj) + print('gap: ' + str(100 * abs(ub - lb) / abs(ub)) + ' %') + + diff --git a/pyomo/contrib/coramin/examples/rosenbrock.py b/pyomo/contrib/coramin/examples/rosenbrock.py new file mode 100644 index 00000000000..e0a58f860d6 --- /dev/null +++ b/pyomo/contrib/coramin/examples/rosenbrock.py @@ -0,0 +1,63 @@ +import pyomo.environ as pe +import coramin + + +def create_nlp(a, b): + # Create the nlp + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(-20.0, 20.0)) + m.y = pe.Var(bounds=(-20.0, 20.0)) + + m.objective = pe.Objective(expr=(a - m.x)**2 + b*(m.y - m.x**2)**2) + + return m + + +def create_relaxation(a, b): + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(-20.0, 20.0)) + m.x_sq = pe.Var() + m.y = pe.Var(bounds=(-20.0, 20.0)) + m.z = pe.Var() + + m.objective = pe.Objective(expr=(a - m.x)**2 + b*m.z**2) + m.con1 = pe.Constraint(expr=m.z == m.y - m.x_sq) + m.x_sq_con = coramin.relaxations.PWXSquaredRelaxation() + m.x_sq_con.build(x=m.x, aux_var=m.x_sq, use_linear_relaxation=True) + + return m + + +def main(): + a = 1 + b = 1 + nlp = create_nlp(a, b) + rel = create_relaxation(a, b) + + nlp_opt = pe.SolverFactory('ipopt') + rel_opt = pe.SolverFactory('gurobi_direct') + + res = nlp_opt.solve(nlp, tee=False) + assert res.solver.termination_condition == pe.TerminationCondition.optimal + ub = pe.value(nlp.objective) + + res = rel_opt.solve(rel, tee=False) + assert res.solver.termination_condition == pe.TerminationCondition.optimal + lb = pe.value(rel.objective) + + print('lb: ', lb) + print('ub: ', ub) + + print('nlp results:') + print('--------------------------') + nlp.x.pprint() + nlp.y.pprint() + + print('relaxation results:') + print('--------------------------') + rel.x.pprint() + rel.y.pprint() + + +if __name__ == '__main__': + main() diff --git a/pyomo/contrib/coramin/relaxations/__init__.py b/pyomo/contrib/coramin/relaxations/__init__.py new file mode 100644 index 00000000000..5f8f47e4463 --- /dev/null +++ b/pyomo/contrib/coramin/relaxations/__init__.py @@ -0,0 +1,12 @@ +from .relaxations_base import BaseRelaxation, BaseRelaxationData, BasePWRelaxation, BasePWRelaxationData +from .mccormick import PWMcCormickRelaxation, PWMcCormickRelaxationData +from .segments import compute_k_segment_points +from .univariate import PWXSquaredRelaxation, PWXSquaredRelaxationData +from .univariate import PWUnivariateRelaxation, PWUnivariateRelaxationData +from .univariate import PWArctanRelaxation, PWArctanRelaxationData +from .univariate import PWSinRelaxation, PWSinRelaxationData +from .univariate import PWCosRelaxation, PWCosRelaxationData +from .auto_relax import relax +from .alphabb import AlphaBBRelaxationData, AlphaBBRelaxation +from .multivariate import MultivariateRelaxationData, MultivariateRelaxation +from .iterators import relaxation_data_objects, nonrelaxation_component_data_objects diff --git a/pyomo/contrib/coramin/relaxations/_utils.py b/pyomo/contrib/coramin/relaxations/_utils.py new file mode 100644 index 00000000000..b8e79e14705 --- /dev/null +++ b/pyomo/contrib/coramin/relaxations/_utils.py @@ -0,0 +1,75 @@ +import logging +import pyomo.environ as pe +import warnings +import math + +logger = logging.getLogger(__name__) +pyo = pe + + +def _get_bnds_tuple(v): + lb = pe.value(v.lb) + ub = pe.value(v.ub) + if lb is None: + lb = -math.inf + if ub is None: + ub = math.inf + + return lb, ub + + +def _get_bnds_list(v): + return list(_get_bnds_tuple(v)) + + +def var_info_str(v): + s = '\tVar: {0}\n'.format(v) + return s + + +def bnds_info_str(vlb, vub): + s = '\tLB: {0}\n'.format(vlb) + s += '\tUB: {0}\n'.format(vub) + return s + + +def x_pts_info_str(_x_pts): + s = '\tx_pts: {0}\n'.format(_x_pts) + return s + + +def check_var_pts(x, x_pts=None): + xlb = pe.value(x.lb) + xub = pe.value(x.ub) + + if xlb is None: + xlb = -math.inf + if xub is None: + xub = math.inf + + raise_error = False + raise_warning = False + msg = None + + if xub < xlb: + msg = 'Lower bound is larger than upper bound:\n' + var_info_str(x) + bnds_info_str(xlb, xub) + raise_error = True + + if x_pts is not None: + ordered = all(x_pts[i] <= x_pts[i+1] for i in range(len(x_pts)-1)) + if not ordered: + msg = 'x_pts must be ordered:\n' + var_info_str(x) + bnds_info_str(xlb, xub) + x_pts_info_str(x_pts) + raise_error = True + + if xlb != x_pts[0] or xub != x_pts[-1]: + msg = ('end points of the x_pts list must be equal to the bounds on the x variable:\n' + var_info_str(x) + + bnds_info_str(xlb, xub) + x_pts_info_str(x_pts)) + raise_error = True + + if raise_error: + logger.error(msg) + raise ValueError(msg) + + if raise_warning: + logger.warning(msg) + warnings.warn(msg) diff --git a/pyomo/contrib/coramin/relaxations/alphabb.py b/pyomo/contrib/coramin/relaxations/alphabb.py new file mode 100644 index 00000000000..e10b7d1320c --- /dev/null +++ b/pyomo/contrib/coramin/relaxations/alphabb.py @@ -0,0 +1,166 @@ +from coramin.utils.coramin_enums import EigenValueBounder, RelaxationSide +from coramin.relaxations.custom_block import declare_custom_block +from coramin.relaxations.relaxations_base import BaseRelaxationData, ComponentWeakRef +from coramin.relaxations.hessian import Hessian +from typing import Optional, Tuple +from pyomo.core.base.var import _GeneralVarData +from pyomo.core.expr.numeric_expr import ExpressionBase +from pyomo.core.expr.visitor import identify_variables +from pyomo.contrib import appsi +from pyomo.core.base.param import ScalarParam, IndexedParam +from pyomo.core.base.set import OrderedScalarSet + + +@declare_custom_block(name='AlphaBBRelaxation') +class AlphaBBRelaxationData(BaseRelaxationData): + def __init__(self, component): + super().__init__(component) + self._xs: Optional[Tuple[_GeneralVarData]] = None + self._aux_var_ref = ComponentWeakRef(None) + self._f_x_expr: Optional[ExpressionBase] = None + self._alphabb_rhs: Optional[ExpressionBase] = None + self._hessian: Optional[Hessian] = None + self._alpha: Optional[ScalarParam] = None + self._var_set: Optional[OrderedScalarSet] = None + self._lb_params: Optional[IndexedParam] = None + self._ub_params: Optional[IndexedParam] = None + + @property + def hessian(self): + return self._hessian + + @property + def _aux_var(self): + return self._aux_var_ref.get_component() + + def get_rhs_vars(self) -> Tuple[_GeneralVarData, ...]: + return self._xs + + def get_rhs_expr(self) -> ExpressionBase: + return self._f_x_expr + + def vars_with_bounds_in_relaxation(self): + if self.is_rhs_convex(): + return list() + else: + return list(self._xs) + + def is_rhs_convex(self): + return self._hessian.get_minimum_eigenvalue() >= 0 + + def is_rhs_concave(self): + return self._hessian.get_maximum_eigenvalue() <= 0 + + def has_convex_underestimator(self): + return self.relaxation_side == RelaxationSide.UNDER + + def has_concave_overestimator(self): + return self.relaxation_side == RelaxationSide.OVER + + def _get_expr_for_oa(self): + return self._alphabb_rhs + + def set_input( + self, + aux_var: _GeneralVarData, + f_x_expr: ExpressionBase, + relaxation_side: RelaxationSide, + use_linear_relaxation: bool = True, + large_coef: float = 1e5, + small_coef: float = 1e-10, + safety_tol: float = 1e-10, + eigenvalue_bounder: EigenValueBounder = EigenValueBounder.LinearProgram, + eigenvalue_opt: Optional[appsi.base.Solver] = None, + hessian: Optional[Hessian] = None, + ): + del self._alpha, self._alphabb_rhs, self._var_set, self._lb_params + del self._ub_params + self._alpha, self._alphabb_rhs, self._var_set = None, None, None + self._lb_params, self._ub_params = None, None + self._relaxation_side = relaxation_side + super().set_input( + relaxation_side=relaxation_side, + use_linear_relaxation=use_linear_relaxation, + large_coef=large_coef, + small_coef=small_coef, + safety_tol=safety_tol + ) + self._xs = tuple(identify_variables(f_x_expr, include_fixed=False)) + self._aux_var_ref.set_component(aux_var) + self._f_x_expr = f_x_expr + if hessian is None: + hessian = Hessian( + expr=f_x_expr, opt=eigenvalue_opt, method=eigenvalue_bounder + ) + self._hessian = hessian + + def build( + self, + aux_var: _GeneralVarData, + f_x_expr: ExpressionBase, + relaxation_side: RelaxationSide, + use_linear_relaxation: bool = True, + large_coef: float = 1e5, + small_coef: float = 1e-10, + safety_tol: float = 1e-10, + eigenvalue_bounder: EigenValueBounder = EigenValueBounder.LinearProgram, + eigenvalue_opt: appsi.base.Solver = None, + ): + self.set_input( + aux_var=aux_var, + f_x_expr=f_x_expr, + relaxation_side=relaxation_side, + use_linear_relaxation=use_linear_relaxation, + large_coef=large_coef, + small_coef=small_coef, + safety_tol=safety_tol, + eigenvalue_bounder=eigenvalue_bounder, + eigenvalue_opt=eigenvalue_opt, + ) + self.rebuild() + + @property + def use_linear_relaxation(self): + return self._use_linear_relaxation + + @use_linear_relaxation.setter + def use_linear_relaxation(self, value): + self._use_linear_relaxation = value + + @property + def relaxation_side(self): + return BaseRelaxationData.relaxation_side.fget(self) + + @relaxation_side.setter + def relaxation_side(self, val): + if val != self.relaxation_side: + raise ValueError('Cannot change the relaxation side of an AlphaBBRelaxation') + if val == RelaxationSide.BOTH: + raise ValueError('AlphaBBRelaxation only supports relaxation sides of UNDER or OVER, not BOTH.') + + def rebuild(self, build_nonlinear_constraint=False, ensure_oa_at_vertices=True): + if self.relaxation_side == RelaxationSide.UNDER: + alpha = max(0, -0.5 * self._hessian.get_minimum_eigenvalue()) + else: + alpha = max(0, 0.5*self._hessian.get_maximum_eigenvalue()) + alpha = -alpha + if self._alpha is None: + del self._alpha, self._alphabb_rhs, self._var_set + del self._lb_params, self._ub_params + self._alpha = ScalarParam(mutable=True) + self._var_set = OrderedScalarSet(initialize=list(range(len(self._xs)))) + self._lb_params = IndexedParam(self._var_set, mutable=True) + self._ub_params = IndexedParam(self._var_set, mutable=True) + alpha_sum = 0 + for ndx, v in enumerate(self._xs): + p_lb = self._lb_params[ndx] + p_ub = self._ub_params[ndx] + alpha_sum += (v - p_lb) * (v - p_ub) + self._alphabb_rhs = self.get_rhs_expr() + self._alpha * alpha_sum + self._alpha.value = alpha + for ndx, v in enumerate(self._xs): + self._lb_params[ndx].value = v.lb + self._ub_params[ndx].value = v.ub + + super().rebuild(build_nonlinear_constraint=build_nonlinear_constraint, + ensure_oa_at_vertices=ensure_oa_at_vertices) diff --git a/pyomo/contrib/coramin/relaxations/auto_relax.py b/pyomo/contrib/coramin/relaxations/auto_relax.py new file mode 100644 index 00000000000..262a7d67471 --- /dev/null +++ b/pyomo/contrib/coramin/relaxations/auto_relax.py @@ -0,0 +1,1381 @@ +import pyomo.environ as pe +from pyomo.common.collections import ComponentMap, ComponentSet +import pyomo.core.expr.numeric_expr as numeric_expr +from pyomo.core.expr.visitor import ExpressionValueVisitor +from pyomo.core.expr.numvalue import ( + nonpyomo_leaf_types, value, NumericValue, is_fixed, polynomial_degree, is_constant, + native_numeric_types +) +from pyomo.core.expr.numeric_expr import ExpressionBase +from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr, fbbt +import math +from pyomo.core.base.constraint import Constraint +import logging +from .relaxations_base import BaseRelaxationData +from .univariate import PWUnivariateRelaxation, PWXSquaredRelaxation, PWCosRelaxation, PWSinRelaxation, PWArctanRelaxation +from .mccormick import PWMcCormickRelaxation +from .multivariate import MultivariateRelaxation +from .alphabb import AlphaBBRelaxation +from coramin.utils.coramin_enums import RelaxationSide, FunctionShape, Effort, EigenValueBounder +from pyomo.gdp import Disjunct +from pyomo.core.base.expression import _GeneralExpressionData, SimpleExpression +from coramin.relaxations.iterators import nonrelaxation_component_data_objects +from pyomo.contrib import appsi +from pyomo.repn.standard_repn import generate_standard_repn +from pyomo.contrib.fbbt import interval +from pyomo.core.expr.compare import convert_expression_to_prefix_notation +from .split_expr import split_expr +from coramin.utils.pyomo_utils import simplify_expr, active_vars +from .hessian import Hessian +from typing import MutableMapping, Tuple, Union, Optional +from pyomo.core.base.block import _BlockData +from .iterators import relaxation_data_objects + + +logger = logging.getLogger(__name__) + + +class Hashable: + def __init__(self, *args): + entries = list() + for i in args: + itype = type(i) + if itype is tuple or itype in nonpyomo_leaf_types: + entries.append(i) + elif isinstance(i, NumericValue): + entries.append(id(i)) + else: + raise NotImplementedError( + f'unexpected entry: {str(i)}') + self.entries = entries + self.hashable_entries = tuple(entries) + + def __eq__(self, other): + if isinstance(other, Hashable): + return self.entries == other.entries + return False + + def __hash__(self): + return hash(self.hashable_entries) + + +class RelaxationException(Exception): + pass + + +class RelaxationCounter(object): + def __init__(self): + self.count = 0 + + def increment(self): + self.count += 1 + + def __str__(self): + return str(self.count) + + +def compute_float_bounds_on_expr(expr): + lb, ub = compute_bounds_on_expr(expr) + if lb is None: + lb = -math.inf + if ub is None: + ub = math.inf + + return lb, ub + + +def replace_sub_expression_with_aux_var(arg, parent_block): + if type(arg) in nonpyomo_leaf_types: + return arg + elif arg.is_expression_type(): + _var = parent_block.aux_vars.add() + _con = parent_block.aux_cons.add(_var == arg) + fbbt(_con) + return _var + else: + return arg + + +def _get_aux_var(parent_block, expr): + _aux_var = parent_block.aux_vars.add() + lb, ub = compute_bounds_on_expr(expr) + _aux_var.setlb(lb) + _aux_var.setub(ub) + try: + expr_value = pe.value(expr, exception=False) + except ArithmeticError: + expr_value = None + if expr_value is not None and pe.value(_aux_var, exception=False) is None: + _aux_var.set_value(expr_value, skip_validation=True) + return _aux_var + + +def _relax_leaf_to_root_ProductExpression(node, values, aux_var_map, degree_map, parent_block, relaxation_side_map, counter): + arg1, arg2 = values + + # The purpose of the next bit of code is to find common quadratic terms. For example, suppose we are relaxing + # a model with the following two constraints: + # + # w1 - x*y = 0 + # w2 + 3*x*y = 0 + # + # we want to end up with + # + # w1 - aux1 = 0 + # w2 + 3*aux1 = 0 + # aux1 = x*y + # + # rather than + # + # w1 - aux1 = 0 + # w2 + 3*aux2 = 0 + # aux1 = x*y + # aux2 = x*y + # + + h1 = Hashable(arg1, arg2, 'mul') + h2 = Hashable(arg2, arg1, 'mul') + if h1 in aux_var_map or h2 in aux_var_map: + if h1 in aux_var_map: + _aux_var, relaxation = aux_var_map[h1] + else: + _aux_var, relaxation = aux_var_map[h2] + relaxation_side = relaxation_side_map[node] + if relaxation_side != relaxation.relaxation_side: + relaxation.relaxation_side = RelaxationSide.BOTH + res = _aux_var + degree_map[res] = 1 + else: + degree_1 = degree_map[arg1] + degree_2 = degree_map[arg2] + if degree_1 == 0: + res = arg1 * arg2 + degree_map[res] = degree_2 + elif degree_2 == 0: + res = arg2 * arg1 + degree_map[res] = degree_1 + elif arg1.__class__ == numeric_expr.MonomialTermExpression or arg2.__class__ == numeric_expr.MonomialTermExpression: + if arg1.__class__ == numeric_expr.MonomialTermExpression: + coef1, arg1 = arg1.args + else: + coef1 = 1 + if arg2.__class__ == numeric_expr.MonomialTermExpression: + coef2, arg2 = arg2.args + else: + coef2 = 1 + coef = coef1 * coef2 + _new_relaxation_side_map = ComponentMap() + _reformulated = coef * (arg1 * arg2) + _new_relaxation_side_map[_reformulated] = relaxation_side_map[node] + res = _relax_expr(expr=_reformulated, aux_var_map=aux_var_map, parent_block=parent_block, + relaxation_side_map=_new_relaxation_side_map, counter=counter, degree_map=degree_map) + degree_map[res] = 1 + elif arg1 is arg2: + # reformulate arg1 * arg2 as arg1**2 + _new_relaxation_side_map = ComponentMap() + _reformulated = arg1**2 + _new_relaxation_side_map[_reformulated] = relaxation_side_map[node] + res = _relax_expr(expr=_reformulated, aux_var_map=aux_var_map, parent_block=parent_block, + relaxation_side_map=_new_relaxation_side_map, counter=counter, degree_map=degree_map) + degree_map[res] = 1 + else: + _aux_var = _get_aux_var(parent_block, arg1 * arg2) + arg1 = replace_sub_expression_with_aux_var(arg1, parent_block) + arg2 = replace_sub_expression_with_aux_var(arg2, parent_block) + relaxation_side = relaxation_side_map[node] + relaxation = PWMcCormickRelaxation() + relaxation.set_input(x1=arg1, x2=arg2, aux_var=_aux_var, relaxation_side=relaxation_side) + aux_var_map[h1] = (_aux_var, relaxation) + setattr(parent_block.relaxations, 'rel'+str(counter), relaxation) + counter.increment() + res = _aux_var + degree_map[res] = 1 + return res + + +def _relax_leaf_to_root_DivisionExpression(node, values, aux_var_map, degree_map, parent_block, relaxation_side_map, counter): + arg1, arg2 = values + h1 = Hashable(arg1, arg2, 'div') + if arg1.__class__ == numeric_expr.MonomialTermExpression: + coef1, arg1 = arg1.args + else: + coef1 = 1 + if arg2.__class__ == numeric_expr.MonomialTermExpression: + coef2, arg2 = arg2.args + else: + coef2 = 1 + coef = coef1/coef2 + degree_1 = degree_map[arg1] + degree_2 = degree_map[arg2] + + if degree_2 == 0: + res = (coef / arg2) * arg1 + degree_map[res] = degree_1 + return res + elif h1 in aux_var_map: + _aux_var, relaxation = aux_var_map[h1] + relaxation_side = relaxation_side_map[node] + if relaxation_side != relaxation.relaxation_side: + relaxation.relaxation_side = RelaxationSide.BOTH + res = coef * _aux_var + degree_map[_aux_var] = 1 + degree_map[res] = 1 + return res + elif degree_1 == 0: + h2 = Hashable(arg2, 'reciprocal') + if h2 in aux_var_map: + _aux_var, relaxation = aux_var_map[h2] + relaxation_side = relaxation_side_map[node] + if relaxation_side != relaxation.relaxation_side: + relaxation.relaxation_side = RelaxationSide.BOTH + res = coef * arg1 * _aux_var + degree_map[_aux_var] = 1 + degree_map[res] = 1 + return res + else: + _aux_var = _get_aux_var(parent_block, 1/arg2) + arg2 = replace_sub_expression_with_aux_var(arg2, parent_block) + relaxation_side = relaxation_side_map[node] + degree_map[_aux_var] = 1 + if compute_float_bounds_on_expr(arg2)[0] > 0: + relaxation = PWUnivariateRelaxation() + relaxation.set_input(x=arg2, aux_var=_aux_var, relaxation_side=relaxation_side, f_x_expr=1/arg2, + shape=FunctionShape.CONVEX) + elif compute_float_bounds_on_expr(arg2)[1] < 0: + relaxation = PWUnivariateRelaxation() + relaxation.set_input(x=arg2, aux_var=_aux_var, relaxation_side=relaxation_side, f_x_expr=1/arg2, + shape=FunctionShape.CONCAVE) + else: + _one = parent_block.aux_vars.add() + _one.fix(1) + relaxation = PWMcCormickRelaxation() + relaxation.set_input(x1=arg2, x2=_aux_var, aux_var=_one, relaxation_side=relaxation_side) + aux_var_map[h2] = (_aux_var, relaxation) + setattr(parent_block.relaxations, 'rel'+str(counter), relaxation) + counter.increment() + res = coef * arg1 * _aux_var + degree_map[res] = 1 + return res + else: + _aux_var = _get_aux_var(parent_block, arg1 / arg2) + arg1 = replace_sub_expression_with_aux_var(arg1, parent_block) + arg2 = replace_sub_expression_with_aux_var(arg2, parent_block) + relaxation_side = relaxation_side_map[node] + arg2_lb, arg2_ub = compute_float_bounds_on_expr(arg2) + if arg2_lb >= 0: + if relaxation_side == RelaxationSide.UNDER: + relaxation_side = RelaxationSide.OVER + elif relaxation_side == RelaxationSide.OVER: + relaxation_side = RelaxationSide.UNDER + else: + assert relaxation_side == RelaxationSide.BOTH + elif arg2_ub <= 0: + pass + else: + relaxation_side = RelaxationSide.BOTH + relaxation = PWMcCormickRelaxation() + relaxation.set_input(x1=arg2, x2=_aux_var, aux_var=arg1, relaxation_side=relaxation_side) + aux_var_map[h1] = (_aux_var, relaxation) + setattr(parent_block.relaxations, 'rel'+str(counter), relaxation) + counter.increment() + res = coef * _aux_var + degree_map[_aux_var] = 1 + degree_map[res] = 1 + return res + + +def _relax_quadratic(arg1, aux_var_map, relaxation_side, degree_map, parent_block, counter): + _aux_var = _get_aux_var(parent_block, arg1**2) + arg1 = replace_sub_expression_with_aux_var(arg1, parent_block) + degree_map[_aux_var] = 1 + relaxation = PWXSquaredRelaxation() + relaxation.set_input(x=arg1, aux_var=_aux_var, relaxation_side=relaxation_side) + aux_var_map[Hashable(arg1, 2, 'pow')] = (_aux_var, relaxation) + setattr(parent_block.relaxations, 'rel' + str(counter), relaxation) + counter.increment() + return _aux_var + + +def _relax_convex_pow(arg1, arg2, aux_var_map, relaxation_side, degree_map, parent_block, counter, swap=False): + _aux_var = _get_aux_var(parent_block, arg1**arg2) + if swap: + arg2 = replace_sub_expression_with_aux_var(arg2, parent_block) + _x = arg2 + else: + arg1 = replace_sub_expression_with_aux_var(arg1, parent_block) + _x = arg1 + degree_map[_aux_var] = 1 + relaxation = PWUnivariateRelaxation() + relaxation.set_input(x=_x, aux_var=_aux_var, relaxation_side=relaxation_side, f_x_expr=arg1 ** arg2, + shape=FunctionShape.CONVEX) + aux_var_map[Hashable(arg1, arg2, 'pow')] = (_aux_var, relaxation) + setattr(parent_block.relaxations, 'rel' + str(counter), relaxation) + counter.increment() + return _aux_var + + +def _relax_concave_pow(arg1, arg2, aux_var_map, relaxation_side, degree_map, parent_block, counter): + _aux_var = _get_aux_var(parent_block, arg1 ** arg2) + arg1 = replace_sub_expression_with_aux_var(arg1, parent_block) + degree_map[_aux_var] = 1 + relaxation = PWUnivariateRelaxation() + relaxation.set_input(x=arg1, aux_var=_aux_var, relaxation_side=relaxation_side, f_x_expr=arg1 ** arg2, + shape=FunctionShape.CONCAVE) + aux_var_map[Hashable(arg1, arg2, 'pow')] = (_aux_var, relaxation) + setattr(parent_block.relaxations, 'rel' + str(counter), relaxation) + counter.increment() + return _aux_var + + +def _relax_leaf_to_root_PowExpression(node, values, aux_var_map, degree_map, parent_block, relaxation_side_map, counter): + arg1, arg2 = values + h = Hashable(arg1, arg2, 'pow') + if h in aux_var_map: + _aux_var, relaxation = aux_var_map[h] + if relaxation_side_map[node] != relaxation.relaxation_side: + relaxation.relaxation_side = RelaxationSide.BOTH + degree_map[_aux_var] = 1 + return _aux_var + else: + degree1 = degree_map[arg1] + degree2 = degree_map[arg2] + if degree2 == 0: + if degree1 == 0: + res = arg1 ** arg2 + degree_map[res] = 0 + return res + if not is_constant(arg2): + logger.warning('Only constant exponents are supported: ' + str(arg1**arg2) + '\nReplacing ' + str(arg2) + ' with its value.') + arg2 = pe.value(arg2) + if arg2 == 1: + return arg1 + elif arg2 == 0: + res = 1 + degree_map[res] = 0 + return res + elif arg2 == 2: + return _relax_quadratic(arg1=arg1, aux_var_map=aux_var_map, relaxation_side=relaxation_side_map[node], + degree_map=degree_map, parent_block=parent_block, counter=counter) + elif arg2 >= 0: + if arg2 == round(arg2): + if arg2 % 2 == 0 or compute_float_bounds_on_expr(arg1)[0] >= 0: + return _relax_convex_pow(arg1=arg1, arg2=arg2, aux_var_map=aux_var_map, + relaxation_side=relaxation_side_map[node], degree_map=degree_map, + parent_block=parent_block, counter=counter) + elif compute_float_bounds_on_expr(arg1)[1] <= 0: + return _relax_concave_pow(arg1=arg1, arg2=arg2, aux_var_map=aux_var_map, + relaxation_side=relaxation_side_map[node], degree_map=degree_map, + parent_block=parent_block, counter=counter) + else: # reformulate arg1 ** arg2 as arg1 * arg1 ** (arg2 - 1) + _new_relaxation_side_map = ComponentMap() + _reformulated = arg1 * arg1 ** (arg2 - 1) + _new_relaxation_side_map[_reformulated] = relaxation_side_map[node] + res = _relax_expr(expr=_reformulated, aux_var_map=aux_var_map, parent_block=parent_block, + relaxation_side_map=_new_relaxation_side_map, counter=counter, + degree_map=degree_map) + degree_map[res] = 1 + return res + else: + if arg2 < 1: + return _relax_concave_pow(arg1=arg1, arg2=arg2, aux_var_map=aux_var_map, + relaxation_side=relaxation_side_map[node], degree_map=degree_map, + parent_block=parent_block, counter=counter) + else: + return _relax_convex_pow(arg1=arg1, arg2=arg2, aux_var_map=aux_var_map, + relaxation_side=relaxation_side_map[node], degree_map=degree_map, + parent_block=parent_block, counter=counter) + else: + if arg2 == round(arg2): + if compute_float_bounds_on_expr(arg1)[0] >= 0: + return _relax_convex_pow(arg1=arg1, arg2=arg2, aux_var_map=aux_var_map, + relaxation_side=relaxation_side_map[node], degree_map=degree_map, + parent_block=parent_block, counter=counter) + elif compute_float_bounds_on_expr(arg1)[1] <= 0: + if arg2 % 2 == 0: + return _relax_convex_pow(arg1=arg1, arg2=arg2, aux_var_map=aux_var_map, + relaxation_side=relaxation_side_map[node], degree_map=degree_map, + parent_block=parent_block, counter=counter) + else: + return _relax_concave_pow(arg1=arg1, arg2=arg2, aux_var_map=aux_var_map, + relaxation_side=relaxation_side_map[node], degree_map=degree_map, + parent_block=parent_block, counter=counter) + else: + # reformulate arg1 ** arg2 as 1 / arg1 ** (-arg2) + _new_relaxation_side_map = ComponentMap() + _reformulated = 1 / (arg1 ** (-arg2)) + _new_relaxation_side_map[_reformulated] = relaxation_side_map[node] + res = _relax_expr(expr=_reformulated, aux_var_map=aux_var_map, parent_block=parent_block, + relaxation_side_map=_new_relaxation_side_map, counter=counter, + degree_map=degree_map) + degree_map[res] = 1 + return res + else: + assert compute_float_bounds_on_expr(arg1)[0] >= 0 + return _relax_convex_pow(arg1=arg1, arg2=arg2, aux_var_map=aux_var_map, + relaxation_side=relaxation_side_map[node], degree_map=degree_map, + parent_block=parent_block, counter=counter) + elif degree1 == 0: + if not is_constant(arg1): + logger.warning('Found {0} raised to a variable power. However, {0} does not appear to be constant (maybe ' + 'it is or depends on a mutable Param?). Replacing {0} with its value.'.format(str(arg1))) + arg1 = pe.value(arg1) + if arg1 < 0: + raise ValueError('Cannot raise a negative base to a variable exponent: ' + str(arg1**arg2)) + return _relax_convex_pow(arg1=arg1, arg2=arg2, aux_var_map=aux_var_map, + relaxation_side=relaxation_side_map[node], degree_map=degree_map, + parent_block=parent_block, counter=counter, swap=True) + else: + assert compute_float_bounds_on_expr(arg1)[0] >= 0 + _new_relaxation_side_map = ComponentMap() + _reformulated = pe.exp(arg2 * pe.log(arg1)) + _new_relaxation_side_map[_reformulated] = relaxation_side_map[node] + res = _relax_expr(expr=_reformulated, aux_var_map=aux_var_map, parent_block=parent_block, + relaxation_side_map=_new_relaxation_side_map, counter=counter, + degree_map=degree_map) + degree_map[res] = 1 + return res + + +def _relax_leaf_to_root_SumExpression(node, values, aux_var_map, degree_map, parent_block, relaxation_side_map, counter): + res = sum(values) + degree_map[res] = max([degree_map[arg] for arg in values]) + return res + + +def _relax_leaf_to_root_NegationExpression(node, values, aux_var_map, degree_map, parent_block, relaxation_side_map, counter): + arg = values[0] + res = -arg + degree_map[res] = degree_map[arg] + return res + + +def _relax_leaf_to_root_sqrt(node, values, aux_var_map, degree_map, parent_block, relaxation_side_map, counter): + arg = values[0] + _new_relaxation_side_map = ComponentMap() + _reformulated = arg**0.5 + _new_relaxation_side_map[_reformulated] = relaxation_side_map[node] + res = _relax_expr(expr=_reformulated, aux_var_map=aux_var_map, parent_block=parent_block, + relaxation_side_map=_new_relaxation_side_map, counter=counter, + degree_map=degree_map) + degree_map[res] = 1 + return res + + +def _relax_leaf_to_root_exp(node, values, aux_var_map, degree_map, parent_block, relaxation_side_map, counter): + arg = values[0] + degree = degree_map[arg] + if degree == 0: + res = pe.exp(arg) + degree_map[res] = 0 + return res + elif (id(arg), 'exp') in aux_var_map: + _aux_var, relaxation = aux_var_map[id(arg), 'exp'] + relaxation_side = relaxation_side_map[node] + if relaxation_side != relaxation.relaxation_side: + relaxation.relaxation_side = RelaxationSide.BOTH + degree_map[_aux_var] = 1 + return _aux_var + else: + _aux_var = _get_aux_var(parent_block, pe.exp(arg)) + arg = replace_sub_expression_with_aux_var(arg, parent_block) + relaxation_side = relaxation_side_map[node] + degree_map[_aux_var] = 1 + relaxation = PWUnivariateRelaxation() + relaxation.set_input(x=arg, aux_var=_aux_var, relaxation_side=relaxation_side, f_x_expr=pe.exp(arg), + shape=FunctionShape.CONVEX) + aux_var_map[id(arg), 'exp'] = (_aux_var, relaxation) + setattr(parent_block.relaxations, 'rel'+str(counter), relaxation) + counter.increment() + return _aux_var + + +def _relax_leaf_to_root_log(node, values, aux_var_map, degree_map, parent_block, relaxation_side_map, counter): + arg = values[0] + degree = degree_map[arg] + if degree == 0: + res = pe.exp(arg) + degree_map[res] = 0 + return res + elif (id(arg), 'log') in aux_var_map: + _aux_var, relaxation = aux_var_map[id(arg), 'log'] + relaxation_side = relaxation_side_map[node] + if relaxation_side != relaxation.relaxation_side: + relaxation.relaxation_side = RelaxationSide.BOTH + degree_map[_aux_var] = 1 + return _aux_var + else: + _aux_var = _get_aux_var(parent_block, pe.log(arg)) + arg = replace_sub_expression_with_aux_var(arg, parent_block) + relaxation_side = relaxation_side_map[node] + degree_map[_aux_var] = 1 + relaxation = PWUnivariateRelaxation() + relaxation.set_input(x=arg, aux_var=_aux_var, relaxation_side=relaxation_side, f_x_expr=pe.log(arg), + shape=FunctionShape.CONCAVE) + aux_var_map[id(arg), 'log'] = (_aux_var, relaxation) + setattr(parent_block.relaxations, 'rel'+str(counter), relaxation) + counter.increment() + return _aux_var + + +def _relax_leaf_to_root_log10(node, values, aux_var_map, degree_map, parent_block, relaxation_side_map, counter): + arg = values[0] + degree = degree_map[arg] + if degree == 0: + res = pe.exp(arg) + degree_map[res] = 0 + return res + elif (id(arg), 'log10') in aux_var_map: + _aux_var, relaxation = aux_var_map[id(arg), 'log10'] + relaxation_side = relaxation_side_map[node] + if relaxation_side != relaxation.relaxation_side: + relaxation.relaxation_side = RelaxationSide.BOTH + degree_map[_aux_var] = 1 + return _aux_var + else: + _aux_var = _get_aux_var(parent_block, pe.log10(arg)) + arg = replace_sub_expression_with_aux_var(arg, parent_block) + relaxation_side = relaxation_side_map[node] + degree_map[_aux_var] = 1 + relaxation = PWUnivariateRelaxation() + relaxation.set_input(x=arg, aux_var=_aux_var, relaxation_side=relaxation_side, f_x_expr=pe.log10(arg), + shape=FunctionShape.CONCAVE) + aux_var_map[id(arg), 'log10'] = (_aux_var, relaxation) + setattr(parent_block.relaxations, 'rel'+str(counter), relaxation) + counter.increment() + return _aux_var + + +def _relax_leaf_to_root_sin(node, values, aux_var_map, degree_map, parent_block, relaxation_side_map, counter): + arg = values[0] + degree = degree_map[arg] + if degree == 0: + res = pe.sin(arg) + degree_map[res] = 0 + return res + elif (id(arg), 'sin') in aux_var_map: + _aux_var, relaxation = aux_var_map[id(arg), 'sin'] + relaxation_side = relaxation_side_map[node] + if relaxation_side != relaxation.relaxation_side: + relaxation.relaxation_side = RelaxationSide.BOTH + degree_map[_aux_var] = 1 + return _aux_var + else: + _aux_var = _get_aux_var(parent_block, pe.sin(arg)) + arg = replace_sub_expression_with_aux_var(arg, parent_block) + relaxation_side = relaxation_side_map[node] + degree_map[_aux_var] = 1 + relaxation = PWSinRelaxation() + relaxation.set_input(x=arg, aux_var=_aux_var, relaxation_side=relaxation_side) + aux_var_map[id(arg), 'sin'] = (_aux_var, relaxation) + setattr(parent_block.relaxations, 'rel'+str(counter), relaxation) + counter.increment() + return _aux_var + + +def _relax_leaf_to_root_cos(node, values, aux_var_map, degree_map, parent_block, relaxation_side_map, counter): + arg = values[0] + degree = degree_map[arg] + if degree == 0: + res = pe.cos(arg) + degree_map[res] = 0 + return res + elif (id(arg), 'cos') in aux_var_map: + _aux_var, relaxation = aux_var_map[id(arg), 'cos'] + relaxation_side = relaxation_side_map[node] + if relaxation_side != relaxation.relaxation_side: + relaxation.relaxation_side = RelaxationSide.BOTH + degree_map[_aux_var] = 1 + return _aux_var + else: + _aux_var = _get_aux_var(parent_block, pe.cos(arg)) + arg = replace_sub_expression_with_aux_var(arg, parent_block) + relaxation_side = relaxation_side_map[node] + degree_map[_aux_var] = 1 + relaxation = PWCosRelaxation() + relaxation.set_input(x=arg, aux_var=_aux_var, relaxation_side=relaxation_side) + aux_var_map[id(arg), 'cos'] = (_aux_var, relaxation) + setattr(parent_block.relaxations, 'rel'+str(counter), relaxation) + counter.increment() + return _aux_var + + +def _relax_leaf_to_root_arctan(node, values, aux_var_map, degree_map, parent_block, relaxation_side_map, counter): + arg = values[0] + degree = degree_map[arg] + if degree == 0: + res = pe.atan(arg) + degree_map[res] = 0 + return res + elif (id(arg), 'arctan') in aux_var_map: + _aux_var, relaxation = aux_var_map[id(arg), 'arctan'] + relaxation_side = relaxation_side_map[node] + if relaxation_side != relaxation.relaxation_side: + relaxation.relaxation_side = RelaxationSide.BOTH + degree_map[_aux_var] = 1 + return _aux_var + else: + _aux_var = _get_aux_var(parent_block, pe.atan(arg)) + arg = replace_sub_expression_with_aux_var(arg, parent_block) + relaxation_side = relaxation_side_map[node] + degree_map[_aux_var] = 1 + relaxation = PWArctanRelaxation() + relaxation.set_input(x=arg, aux_var=_aux_var, relaxation_side=relaxation_side) + aux_var_map[id(arg), 'arctan'] = (_aux_var, relaxation) + setattr(parent_block.relaxations, 'rel'+str(counter), relaxation) + counter.increment() + return _aux_var + + +def _relax_leaf_to_root_tan(node, values, aux_var_map, degree_map, parent_block, relaxation_side_map, counter): + arg = values[0] + degree = degree_map[arg] + if degree == 0: + res = pe.tan(arg) + degree_map[res] = 0 + return res + elif (id(arg), 'tan') in aux_var_map: + _aux_var, relaxation = aux_var_map[id(arg), 'tan'] + relaxation_side = relaxation_side_map[node] + if relaxation_side != relaxation.relaxation_side: + relaxation.relaxation_side = RelaxationSide.BOTH + degree_map[_aux_var] = 1 + return _aux_var + else: + _aux_var = _get_aux_var(parent_block, pe.tan(arg)) + arg = replace_sub_expression_with_aux_var(arg, parent_block) + relaxation_side = relaxation_side_map[node] + degree_map[_aux_var] = 1 + + if arg.lb >=0 and arg.ub <= math.pi/2: + relaxation = PWUnivariateRelaxation() + relaxation.set_input( + x=arg, aux_var=_aux_var, shape=FunctionShape.CONVEX, + f_x_expr=pe.tan(arg), relaxation_side=relaxation_side + ) + elif arg.lb >= -math.pi/2 and arg.ub <= 0: + relaxation = PWUnivariateRelaxation() + relaxation.set_input( + x=arg, aux_var=_aux_var, shape=FunctionShape.CONCAVE, + f_x_expr=pe.tan(arg), relaxation_side=relaxation_side + ) + else: + raise NotImplementedError('Use alpha-BB here') + aux_var_map[id(arg), 'tan'] = (_aux_var, relaxation) + setattr(parent_block.relaxations, 'rel'+str(counter), relaxation) + counter.increment() + return _aux_var + + +_unary_leaf_to_root_map = dict() +_unary_leaf_to_root_map['exp'] = _relax_leaf_to_root_exp +_unary_leaf_to_root_map['log'] = _relax_leaf_to_root_log +_unary_leaf_to_root_map['log10'] = _relax_leaf_to_root_log10 +_unary_leaf_to_root_map['sin'] = _relax_leaf_to_root_sin +_unary_leaf_to_root_map['cos'] = _relax_leaf_to_root_cos +_unary_leaf_to_root_map['atan'] = _relax_leaf_to_root_arctan +_unary_leaf_to_root_map['sqrt'] = _relax_leaf_to_root_sqrt +_unary_leaf_to_root_map['tan'] = _relax_leaf_to_root_tan + + +def _relax_leaf_to_root_UnaryFunctionExpression(node, values, aux_var_map, degree_map, parent_block, relaxation_side_map, counter): + if node.getname() in _unary_leaf_to_root_map: + return _unary_leaf_to_root_map[node.getname()](node=node, values=values, aux_var_map=aux_var_map, + degree_map=degree_map, parent_block=parent_block, + relaxation_side_map=relaxation_side_map, counter=counter) + else: + raise NotImplementedError('Cannot automatically relax ' + str(node)) + + +def _relax_leaf_to_root_GeneralExpression(node, values, aux_var_map, degree_map, parent_block, relaxation_side_map, counter): + arg = values[0] + return arg + + +_relax_leaf_to_root_map = dict() +_relax_leaf_to_root_map[numeric_expr.ProductExpression] = _relax_leaf_to_root_ProductExpression +_relax_leaf_to_root_map[numeric_expr.SumExpression] = _relax_leaf_to_root_SumExpression +_relax_leaf_to_root_map[numeric_expr.LinearExpression] = _relax_leaf_to_root_SumExpression +_relax_leaf_to_root_map[numeric_expr.MonomialTermExpression] = _relax_leaf_to_root_ProductExpression +_relax_leaf_to_root_map[numeric_expr.NegationExpression] = _relax_leaf_to_root_NegationExpression +_relax_leaf_to_root_map[numeric_expr.PowExpression] = _relax_leaf_to_root_PowExpression +_relax_leaf_to_root_map[numeric_expr.DivisionExpression] = _relax_leaf_to_root_DivisionExpression +_relax_leaf_to_root_map[numeric_expr.UnaryFunctionExpression] = _relax_leaf_to_root_UnaryFunctionExpression +_relax_leaf_to_root_map[numeric_expr.NPV_ProductExpression] = _relax_leaf_to_root_ProductExpression +_relax_leaf_to_root_map[numeric_expr.NPV_SumExpression] = _relax_leaf_to_root_SumExpression +_relax_leaf_to_root_map[numeric_expr.NPV_NegationExpression] = _relax_leaf_to_root_NegationExpression +_relax_leaf_to_root_map[numeric_expr.NPV_PowExpression] = _relax_leaf_to_root_PowExpression +_relax_leaf_to_root_map[numeric_expr.NPV_DivisionExpression] = _relax_leaf_to_root_DivisionExpression +_relax_leaf_to_root_map[numeric_expr.NPV_UnaryFunctionExpression] = _relax_leaf_to_root_UnaryFunctionExpression +_relax_leaf_to_root_map[_GeneralExpressionData] = _relax_leaf_to_root_GeneralExpression +_relax_leaf_to_root_map[SimpleExpression] = _relax_leaf_to_root_GeneralExpression + + +def _relax_root_to_leaf_ProductExpression(node, relaxation_side_map): + arg1, arg2 = node.args + if is_fixed(arg1): + relaxation_side_map[arg1] = RelaxationSide.BOTH + if isinstance(arg1, numeric_expr.ProductExpression): # see Pyomo issue #1147 + arg1_arg1 = arg1.args[0] + arg1_arg2 = arg1.args[1] + try: + arg1_arg1_val = pe.value(arg1_arg1) + except ValueError: + arg1_arg1_val = None + try: + arg1_arg2_val = pe.value(arg1_arg2) + except ValueError: + arg1_arg2_val = None + if arg1_arg1_val == 0 or arg1_arg2_val == 0: + arg1_val = 0 + else: + arg1_val = pe.value(arg1) + else: + arg1_val = pe.value(arg1) + if arg1_val >= 0: + relaxation_side_map[arg2] = relaxation_side_map[node] + else: + if relaxation_side_map[node] == RelaxationSide.UNDER: + relaxation_side_map[arg2] = RelaxationSide.OVER + elif relaxation_side_map[node] == RelaxationSide.OVER: + relaxation_side_map[arg2] = RelaxationSide.UNDER + else: + relaxation_side_map[arg2] = RelaxationSide.BOTH + elif is_fixed(arg2): + relaxation_side_map[arg2] = RelaxationSide.BOTH + if isinstance(arg2, numeric_expr.ProductExpression): # see Pyomo issue #1147 + arg2_arg1 = arg2.args[0] + arg2_arg2 = arg2.args[1] + try: + arg2_arg1_val = pe.value(arg2_arg1) + except ValueError: + arg2_arg1_val = None + try: + arg2_arg2_val = pe.value(arg2_arg2) + except ValueError: + arg2_arg2_val = None + if arg2_arg1_val == 0 or arg2_arg2_val == 0: + arg2_val = 0 + else: + arg2_val = pe.value(arg2) + else: + arg2_val = pe.value(arg2) + if arg2_val >= 0: + relaxation_side_map[arg1] = relaxation_side_map[node] + else: + if relaxation_side_map[node] == RelaxationSide.UNDER: + relaxation_side_map[arg1] = RelaxationSide.OVER + elif relaxation_side_map[node] == RelaxationSide.OVER: + relaxation_side_map[arg1] = RelaxationSide.UNDER + else: + relaxation_side_map[arg1] = RelaxationSide.BOTH + else: + relaxation_side_map[arg1] = RelaxationSide.BOTH + relaxation_side_map[arg2] = RelaxationSide.BOTH + + +def _relax_root_to_leaf_DivisionExpression(node, relaxation_side_map): + arg1, arg2 = node.args + relaxation_side_map[arg1] = RelaxationSide.BOTH + relaxation_side_map[arg2] = RelaxationSide.BOTH + + +def _relax_root_to_leaf_SumExpression(node, relaxation_side_map): + relaxation_side = relaxation_side_map[node] + + for arg in node.args: + relaxation_side_map[arg] = relaxation_side + + +def _relax_root_to_leaf_NegationExpression(node, relaxation_side_map): + arg = node.args[0] + relaxation_side = relaxation_side_map[node] + if relaxation_side == RelaxationSide.BOTH: + relaxation_side_map[arg] = RelaxationSide.BOTH + elif relaxation_side == RelaxationSide.UNDER: + relaxation_side_map[arg] = RelaxationSide.OVER + else: + assert relaxation_side == RelaxationSide.OVER + relaxation_side_map[arg] = RelaxationSide.UNDER + + +def _relax_root_to_leaf_PowExpression(node, relaxation_side_map): + arg1, arg2 = node.args + relaxation_side_map[arg1] = RelaxationSide.BOTH + relaxation_side_map[arg2] = RelaxationSide.BOTH + + +def _relax_root_to_leaf_sqrt(node, relaxation_side_map): + arg = node.args[0] + relaxation_side_map[arg] = relaxation_side_map[node] + + +def _relax_root_to_leaf_exp(node, relaxation_side_map): + arg = node.args[0] + relaxation_side_map[arg] = relaxation_side_map[node] + + +def _relax_root_to_leaf_log(node, relaxation_side_map): + arg = node.args[0] + relaxation_side_map[arg] = relaxation_side_map[node] + + +def _relax_root_to_leaf_log10(node, relaxation_side_map): + arg = node.args[0] + relaxation_side_map[arg] = relaxation_side_map[node] + + +def _relax_root_to_leaf_sin(node, relaxation_side_map): + arg = node.args[0] + relaxation_side_map[arg] = RelaxationSide.BOTH + + +def _relax_root_to_leaf_cos(node, relaxation_side_map): + arg = node.args[0] + relaxation_side_map[arg] = RelaxationSide.BOTH + + +def _relax_root_to_leaf_arctan(node, relaxation_side_map): + arg = node.args[0] + relaxation_side_map[arg] = RelaxationSide.BOTH + + +def _relax_root_to_leaf_tan(node, relaxation_side_map): + arg = node.args[0] + relaxation_side_map[arg] = RelaxationSide.BOTH + + +_unary_root_to_leaf_map = dict() +_unary_root_to_leaf_map['exp'] = _relax_root_to_leaf_exp +_unary_root_to_leaf_map['log'] = _relax_root_to_leaf_log +_unary_root_to_leaf_map['log10'] = _relax_root_to_leaf_log10 +_unary_root_to_leaf_map['sin'] = _relax_root_to_leaf_sin +_unary_root_to_leaf_map['cos'] = _relax_root_to_leaf_cos +_unary_root_to_leaf_map['atan'] = _relax_root_to_leaf_arctan +_unary_root_to_leaf_map['sqrt'] = _relax_root_to_leaf_sqrt +_unary_root_to_leaf_map['tan'] = _relax_root_to_leaf_tan + + +def _relax_root_to_leaf_UnaryFunctionExpression(node, relaxation_side_map): + if node.getname() in _unary_root_to_leaf_map: + _unary_root_to_leaf_map[node.getname()](node, relaxation_side_map) + else: + raise NotImplementedError('Cannot automatically relax ' + str(node)) + + +def _relax_root_to_leaf_GeneralExpression(node, relaxation_side_map): + relaxation_side = relaxation_side_map[node] + relaxation_side_map[node.expr] = relaxation_side + + +_relax_root_to_leaf_map = dict() +_relax_root_to_leaf_map[numeric_expr.ProductExpression] = _relax_root_to_leaf_ProductExpression +_relax_root_to_leaf_map[numeric_expr.SumExpression] = _relax_root_to_leaf_SumExpression +_relax_root_to_leaf_map[numeric_expr.LinearExpression] = _relax_root_to_leaf_SumExpression +_relax_root_to_leaf_map[numeric_expr.MonomialTermExpression] = _relax_root_to_leaf_ProductExpression +_relax_root_to_leaf_map[numeric_expr.NegationExpression] = _relax_root_to_leaf_NegationExpression +_relax_root_to_leaf_map[numeric_expr.PowExpression] = _relax_root_to_leaf_PowExpression +_relax_root_to_leaf_map[numeric_expr.DivisionExpression] = _relax_root_to_leaf_DivisionExpression +_relax_root_to_leaf_map[numeric_expr.UnaryFunctionExpression] = _relax_root_to_leaf_UnaryFunctionExpression +_relax_root_to_leaf_map[numeric_expr.NPV_ProductExpression] = _relax_root_to_leaf_ProductExpression +_relax_root_to_leaf_map[numeric_expr.NPV_SumExpression] = _relax_root_to_leaf_SumExpression +_relax_root_to_leaf_map[numeric_expr.NPV_NegationExpression] = _relax_root_to_leaf_NegationExpression +_relax_root_to_leaf_map[numeric_expr.NPV_PowExpression] = _relax_root_to_leaf_PowExpression +_relax_root_to_leaf_map[numeric_expr.NPV_DivisionExpression] = _relax_root_to_leaf_DivisionExpression +_relax_root_to_leaf_map[numeric_expr.NPV_UnaryFunctionExpression] = _relax_root_to_leaf_UnaryFunctionExpression +_relax_root_to_leaf_map[_GeneralExpressionData] = _relax_root_to_leaf_GeneralExpression +_relax_root_to_leaf_map[SimpleExpression] = _relax_root_to_leaf_GeneralExpression + + +class _FactorableRelaxationVisitor(ExpressionValueVisitor): + """ + This walker generates new constraints with nonlinear terms replaced by + auxiliary variables, and relaxations relating the auxilliary variables to + the original variables. + """ + def __init__(self, aux_var_map, parent_block, relaxation_side_map, counter, degree_map): + self.aux_var_map = aux_var_map + self.parent_block = parent_block + self.relaxation_side_map = relaxation_side_map + self.counter = counter + self.degree_map = degree_map + + def visit(self, node, values): + if node.__class__ in _relax_leaf_to_root_map: + res = _relax_leaf_to_root_map[node.__class__](node, values, self.aux_var_map, self.degree_map, + self.parent_block, self.relaxation_side_map, self.counter) + return res + else: + raise NotImplementedError('Cannot relax an expression of type ' + str(type(node))) + + def visiting_potential_leaf(self, node): + if node.__class__ in nonpyomo_leaf_types: + self.degree_map[node] = 0 + return True, node + + if node.is_variable_type(): + if node.fixed: + self.degree_map[node] = 0 + else: + self.degree_map[node] = 1 + return True, node + + if not node.is_expression_type(): + self.degree_map[node] = 0 + return True, node + + if node.__class__ in _relax_root_to_leaf_map: + _relax_root_to_leaf_map[node.__class__](node, self.relaxation_side_map) + else: + raise NotImplementedError('Cannot relax an expression of type ' + str(type(node))) + + return False, None + + +def _get_prefix_notation(expr): + pn = convert_expression_to_prefix_notation(expr, include_named_exprs=False) + res = list() + for i in pn: + itype = type(i) + if itype is tuple or itype in nonpyomo_leaf_types: + res.append(i) + elif isinstance(i, NumericValue): + if i.is_fixed(): + res.append(pe.value(i)) + else: + assert i.is_variable_type() + res.append(id(i)) + else: + raise NotImplementedError(f'unexpected entry in prefix notation: {str(i)}') + return tuple(res) + + +def _relax_expr(expr, aux_var_map, parent_block, relaxation_side_map, counter, degree_map): + visitor = _FactorableRelaxationVisitor(aux_var_map=aux_var_map, parent_block=parent_block, + relaxation_side_map=relaxation_side_map, counter=counter, + degree_map=degree_map) + new_expr = visitor.dfs_postorder_stack(expr) + return new_expr + + +def _relax_split_expr( + expr: ExpressionBase, + aux_var_map: MutableMapping[ + Tuple, + Tuple[NumericValue, + Union[BaseRelaxationData, + Tuple[BaseRelaxationData, BaseRelaxationData]]] + ], + parent_block: _BlockData, + relaxation_side_map: MutableMapping[NumericValue, RelaxationSide], + counter: RelaxationCounter, + degree_map: MutableMapping[NumericValue, int], + eigenvalue_bounder: EigenValueBounder, + max_vars_per_alpha_bb: int, + max_eigenvalue_for_alpha_bb: float, + eigenvalue_opt: Optional[appsi.base.Solver], +) -> NumericValue: + relaxation_side = relaxation_side_map[expr] + hessian = Hessian(expr, opt=eigenvalue_opt, method=eigenvalue_bounder) + vlist = hessian.variables() + min_eig = hessian.get_minimum_eigenvalue() + max_eig = hessian.get_maximum_eigenvalue() + is_convex = min_eig >= 0 + is_concave = max_eig <= 0 + + all_vars_bounded = True + for v in vlist: + v_lb, v_ub = v.bounds + if v_lb is None or v_ub is None: + all_vars_bounded = False + break + + if len(vlist) == 1 and (is_convex or is_concave): + pn = _get_prefix_notation(expr) + if pn in aux_var_map: + new_expr, relaxation = aux_var_map[pn] + if relaxation_side != relaxation.relaxation_side: + relaxation.relaxation_side = RelaxationSide.BOTH + else: + new_expr = _get_aux_var(parent_block, expr) + relaxation = PWUnivariateRelaxation() + if is_convex: + shape = FunctionShape.CONVEX + else: + shape = FunctionShape.CONCAVE + relaxation.set_input( + x=vlist[0], aux_var=new_expr, relaxation_side=relaxation_side, + f_x_expr=expr, shape=shape, + ) + aux_var_map[pn] = (new_expr, relaxation) + setattr(parent_block.relaxations, 'rel' + str(counter), relaxation) + counter.increment() + degree_map[new_expr] = 1 + elif ((is_convex and relaxation_side == RelaxationSide.UNDER) + or (is_concave and relaxation_side == RelaxationSide.OVER)): + pn = _get_prefix_notation(expr) + if pn in aux_var_map: + new_expr, (underestimator, overestimator) = aux_var_map[pn] + else: + new_expr, underestimator, overestimator = None, None, None + if new_expr is None: + new_expr = _get_aux_var(parent_block, expr) + if ( + (is_convex and underestimator is None) + or (is_concave and overestimator is None) + ): + relaxation = MultivariateRelaxation() + if is_convex: + shape = FunctionShape.CONVEX + underestimator = relaxation + else: + shape = FunctionShape.CONCAVE + overestimator = relaxation + relaxation.set_input( + aux_var=new_expr, shape=shape, f_x_expr=expr, + ) + aux_var_map[pn] = (new_expr, (underestimator, overestimator)) + setattr(parent_block.relaxations, 'rel' + str(counter), relaxation) + counter.increment() + degree_map[new_expr] = 1 + elif ( + all_vars_bounded + and len(vlist) <= max_vars_per_alpha_bb + and ( + (relaxation_side == RelaxationSide.UNDER and min_eig >= -abs(max_eigenvalue_for_alpha_bb)) + or (relaxation_side == RelaxationSide.OVER and max_eig <= abs(max_eigenvalue_for_alpha_bb)) + ) + ): + pn = _get_prefix_notation(expr) + if pn in aux_var_map: + new_expr, (underestimator, overestimator) = aux_var_map[pn] + else: + new_expr, underestimator, overestimator = None, None, None + if new_expr is None: + new_expr = _get_aux_var(parent_block, expr) + if ( + (relaxation_side == RelaxationSide.UNDER and underestimator is None) + or (relaxation_side == RelaxationSide.OVER and overestimator is None) + ): + relaxation = AlphaBBRelaxation() + relaxation.set_input( + aux_var=new_expr, + f_x_expr=expr, + relaxation_side=relaxation_side, + hessian=hessian, + ) + if relaxation_side == RelaxationSide.UNDER: + underestimator = relaxation + else: + overestimator = relaxation + aux_var_map[pn] = (new_expr, (underestimator, overestimator)) + setattr(parent_block.relaxations, 'rel' + str(counter), relaxation) + counter.increment() + degree_map[new_expr] = 1 + else: + visitor = _FactorableRelaxationVisitor(aux_var_map=aux_var_map, + parent_block=parent_block, + relaxation_side_map=relaxation_side_map, + counter=counter, + degree_map=degree_map) + new_expr = visitor.dfs_postorder_stack(expr) + return new_expr + + +def _relax_expr_with_convexity_check( + orig_expr: ExpressionBase, + aux_var_map: MutableMapping[ + Tuple, + Tuple[NumericValue, + Union[BaseRelaxationData, + Tuple[BaseRelaxationData, BaseRelaxationData]]] + ], + parent_block: _BlockData, + relaxation_side_map: MutableMapping[NumericValue, RelaxationSide], + counter: RelaxationCounter, + degree_map: MutableMapping[NumericValue, int], + perform_expression_simplification: bool, + eigenvalue_bounder: EigenValueBounder, + max_vars_per_alpha_bb: int, + max_eigenvalue_for_alpha_bb: float, + eigenvalue_opt: Optional[appsi.base.Solver], +): + if relaxation_side_map[orig_expr] == RelaxationSide.BOTH: + res_list = [] + for side in [RelaxationSide.UNDER, RelaxationSide.OVER]: + relaxation_side_map[orig_expr] = side + tmp_res = _relax_expr_with_convexity_check( + orig_expr=orig_expr, + aux_var_map=aux_var_map, + parent_block=parent_block, + relaxation_side_map=relaxation_side_map, + counter=counter, + degree_map=degree_map, + perform_expression_simplification=perform_expression_simplification, + eigenvalue_bounder=eigenvalue_bounder, + max_vars_per_alpha_bb=max_vars_per_alpha_bb, + max_eigenvalue_for_alpha_bb=max_eigenvalue_for_alpha_bb, + eigenvalue_opt=eigenvalue_opt, + ) + res_list.append(tmp_res) + linking_expr = res_list[0] - res_list[1] + linking_repn = generate_standard_repn(linking_expr, compute_values=False, quadratic=True) + linking_expr = linking_repn.to_expression() + if is_constant(linking_expr): + assert value(linking_expr) == 0 + else: + parent_block.aux_cons.add(linking_repn.to_expression() == 0) + res = res_list[0] + relaxation_side_map[orig_expr] = RelaxationSide.BOTH + else: + if perform_expression_simplification: + _expr = simplify_expr(orig_expr) + else: + _expr = orig_expr + list_of_exprs = split_expr(_expr) + list_of_new_exprs = list() + + for expr in list_of_exprs: + relaxation_side_map[expr] = relaxation_side_map[orig_expr] + new_expr = _relax_split_expr( + expr=expr, + aux_var_map=aux_var_map, + parent_block=parent_block, + relaxation_side_map=relaxation_side_map, + counter=counter, + degree_map=degree_map, + eigenvalue_bounder=eigenvalue_bounder, + max_vars_per_alpha_bb=max_vars_per_alpha_bb, + max_eigenvalue_for_alpha_bb=max_eigenvalue_for_alpha_bb, + eigenvalue_opt=eigenvalue_opt, + ) + list_of_new_exprs.append(new_expr) + res = sum(list_of_new_exprs) + return res + + +def relax( + model, + descend_into=None, + in_place=False, + use_fbbt=True, + fbbt_options=None, + perform_expression_simplification: bool = True, + use_alpha_bb: bool = False, + eigenvalue_bounder: EigenValueBounder = EigenValueBounder.GershgorinWithSimplification, + max_vars_per_alpha_bb: int = 4, + max_eigenvalue_for_alpha_bb: float = 100, + eigenvalue_opt: Optional[appsi.base.Solver] = None, +): + """ + Create a convex relaxation of the model. + + Parameters + ---------- + model: pyomo.core.base.block._BlockData or pyomo.core.base.PyomoModel.ConcreteModel + The model or block to be relaxed + descend_into: type or tuple of type, optional + The types of pyomo components that should be checked for constraints to be relaxed. The + default is (Block, Disjunct). + in_place: bool, optional + If False (default=False), model will be cloned, and the clone will be relaxed. + If True, then model will be modified in place. + use_fbbt: bool, optional + If True (default=True), then FBBT will be used to tighten variable bounds. If False, + FBBT will not be used. + fbbt_options: dict, optional + The options to pass to the call to fbbt. See pyomo.contrib.fbbt.fbbt.fbbt for details. + convexity_effort: ConvexityEffort + + Returns + ------- + m: pyomo.core.base.block._BlockData or pyomo.core.base.PyomoModel.ConcreteModel + The relaxed model + """ + """ + For now, we will use FBBT both before relaxing the model and after relaxing the model. The reason we need to + do it before relaxing the model is that the variable bounds will affect the structure of the relaxation. For + example, if we need to relax x**3 and x >= 0, then we know x**3 is convex, and we can relax it as a + convex, univariate function. However, if x can be positive or negative, then x**3 is neither convex nor concave. + In this case, we relax it by reformulating it as x * x**2. The hope is that performing FBBT before relaxing + the model will help identify things like x >= 0 and therefore x**3 is convex. The correct way to do this is to + update the relaxation classes so that the original expression is known, and the best relaxation can be used + anytime the variable bounds are updated. For example, suppose the model is relaxed and, only after OBBT is + performed, we find out x >= 0. We should be able to easily update the relaxation so that x**3 is then relaxed + as a convex univariate function. The reason FBBT needs to be performed after relaxing the model is that + we want to make sure that all of the auxilliary variables introduced get tightened bounds. The correct way to + handle this is to perform FBBT with the original model with suspect, which forms a DAG. Each auxilliary variable + introduced in the relaxed model corresponds to a node in the DAG. If we use suspect, then we can easily + update the bounds of the auxilliary variables without performing FBBT a second time. + """ + if not in_place: + m = model.clone() + else: + m = model + + if fbbt_options is None: + fbbt_options = dict() + + if use_fbbt: + it = appsi.fbbt.IntervalTightener() + for k, v in fbbt_options.items(): + setattr(it.config, k, v) + original_active_vars = ComponentSet(active_vars(m, include_fixed=False)) + it.perform_fbbt(m) + new_active_vars = ComponentSet(active_vars(m, include_fixed=False)) + # some variables may have become stale by deactivating satisfied constraints, + # so we need to fix them. + for v in original_active_vars - new_active_vars: + v.fix(0.5 * (v.lb + v.ub)) + + if descend_into is None: + descend_into = (pe.Block, Disjunct) + + aux_var_map = dict() + counter_dict = dict() + degree_map = ComponentMap() + + for c in nonrelaxation_component_data_objects(m, ctype=Constraint, active=True, descend_into=descend_into, sort=True): + body_degree = polynomial_degree(c.body) + if body_degree is not None: + if body_degree <= 1: + continue + + if c.lower is not None and c.upper is not None: + relaxation_side = RelaxationSide.BOTH + elif c.lower is not None: + relaxation_side = RelaxationSide.OVER + elif c.upper is not None: + relaxation_side = RelaxationSide.UNDER + else: + raise ValueError('Encountered a constraint without a lower or an upper bound: ' + str(c)) + + parent_block = c.parent_block() + + if parent_block in counter_dict: + counter = counter_dict[parent_block] + else: + parent_block.relaxations = pe.Block() + parent_block.aux_vars = pe.VarList() + parent_block.aux_cons = pe.ConstraintList() + counter = RelaxationCounter() + counter_dict[parent_block] = counter + + repn = generate_standard_repn(c.body, quadratic=False, compute_values=False) + assert len(repn.quadratic_vars) == 0 + assert repn.nonlinear_expr is not None + if len(repn.linear_vars) > 0: + new_body = numeric_expr.LinearExpression(constant=repn.constant, linear_coefs=repn.linear_coefs, linear_vars=repn.linear_vars) + else: + new_body = repn.constant + + relaxation_side_map = ComponentMap() + relaxation_side_map[repn.nonlinear_expr] = relaxation_side + + if not use_alpha_bb: + new_body += _relax_expr( + expr=repn.nonlinear_expr, aux_var_map=aux_var_map, + parent_block=parent_block, relaxation_side_map=relaxation_side_map, + counter=counter, degree_map=degree_map + ) + else: + new_body += _relax_expr_with_convexity_check( + orig_expr=repn.nonlinear_expr, aux_var_map=aux_var_map, + parent_block=parent_block, relaxation_side_map=relaxation_side_map, + counter=counter, degree_map=degree_map, + perform_expression_simplification=perform_expression_simplification, + eigenvalue_bounder=eigenvalue_bounder, + max_vars_per_alpha_bb=max_vars_per_alpha_bb, + max_eigenvalue_for_alpha_bb=max_eigenvalue_for_alpha_bb, + eigenvalue_opt=eigenvalue_opt, + ) + lb = c.lower + ub = c.upper + parent_block.aux_cons.add(pe.inequality(lb, new_body, ub)) + parent_component = c.parent_component() + if parent_component.is_indexed(): + del parent_component[c.index()] + else: + parent_block.del_component(c) + + for c in nonrelaxation_component_data_objects(m, ctype=pe.Objective, active=True, descend_into=descend_into, sort=True): + degree = polynomial_degree(c.expr) + if degree is not None: + if degree <= 1: + continue + + if c.sense == pe.minimize: + relaxation_side = RelaxationSide.UNDER + elif c.sense == pe.maximize: + relaxation_side = RelaxationSide.OVER + else: + raise ValueError('Encountered an objective with an unrecognized sense: ' + str(c)) + + parent_block = c.parent_block() + + if parent_block in counter_dict: + counter = counter_dict[parent_block] + else: + parent_block.relaxations = pe.Block() + parent_block.aux_vars = pe.VarList() + parent_block.aux_cons = pe.ConstraintList() + counter = RelaxationCounter() + counter_dict[parent_block] = counter + + if not hasattr(parent_block, 'aux_objectives'): + parent_block.aux_objectives = pe.ObjectiveList() + + repn = generate_standard_repn(c.expr, quadratic=False, compute_values=False) + assert len(repn.quadratic_vars) == 0 + assert repn.nonlinear_expr is not None + if len(repn.linear_vars) > 0: + new_body = numeric_expr.LinearExpression(constant=repn.constant, linear_coefs=repn.linear_coefs, linear_vars=repn.linear_vars) + else: + new_body = repn.constant + + relaxation_side_map = ComponentMap() + relaxation_side_map[repn.nonlinear_expr] = relaxation_side + + if not use_alpha_bb: + new_body += _relax_expr( + expr=repn.nonlinear_expr, aux_var_map=aux_var_map, + parent_block=parent_block, relaxation_side_map=relaxation_side_map, + counter=counter, degree_map=degree_map + ) + else: + new_body += _relax_expr_with_convexity_check( + orig_expr=repn.nonlinear_expr, aux_var_map=aux_var_map, + parent_block=parent_block, relaxation_side_map=relaxation_side_map, + counter=counter, degree_map=degree_map, + perform_expression_simplification=perform_expression_simplification, + eigenvalue_bounder=eigenvalue_bounder, + max_vars_per_alpha_bb=max_vars_per_alpha_bb, + max_eigenvalue_for_alpha_bb=max_eigenvalue_for_alpha_bb, + eigenvalue_opt=eigenvalue_opt, + ) + sense = c.sense + parent_block.aux_objectives.add(new_body, sense=sense) + parent_component = c.parent_component() + if parent_component.is_indexed(): + del parent_component[c.index()] + else: + parent_block.del_component(c) + + if use_fbbt: + for relaxation in relaxation_data_objects(m, descend_into=True, active=True): + relaxation.rebuild(build_nonlinear_constraint=True) + + it = appsi.fbbt.IntervalTightener() + for k, v in fbbt_options.items(): + setattr(it.config, k, v) + it.config.deactivate_satisfied_constraints = False + it.perform_fbbt(m) + + for relaxation in relaxation_data_objects(m, descend_into=True, active=True): + relaxation.use_linear_relaxation = True + relaxation.rebuild() + else: + for relaxation in relaxation_data_objects(m, descend_into=True, active=True): + relaxation.use_linear_relaxation = True + relaxation.rebuild() + + return m diff --git a/pyomo/contrib/coramin/relaxations/copy_relaxation.py b/pyomo/contrib/coramin/relaxations/copy_relaxation.py new file mode 100644 index 00000000000..10b1b83e06f --- /dev/null +++ b/pyomo/contrib/coramin/relaxations/copy_relaxation.py @@ -0,0 +1,123 @@ +from .mccormick import PWMcCormickRelaxationData, PWMcCormickRelaxation +from .univariate import PWXSquaredRelaxationData, PWUnivariateRelaxationData, PWArctanRelaxationData, \ + PWCosRelaxationData, PWSinRelaxationData +from .univariate import PWXSquaredRelaxation, PWUnivariateRelaxation, PWArctanRelaxation, \ + PWCosRelaxation, PWSinRelaxation +from .alphabb import AlphaBBRelaxation, AlphaBBRelaxationData +from .multivariate import MultivariateRelaxationData, MultivariateRelaxation +from pyomo.core.expr.visitor import replace_expressions +from coramin.utils.coramin_enums import FunctionShape + + +def copy_relaxation_with_local_data(rel, old_var_to_new_var_map): + """ + This function copies a relaxation object with new variables. + Note that only what can be set through the set_input and build + methods are copied. For example, piecewise partitioning points + are not copied. + + Parameters + ---------- + rel: coramin.relaxations.relaxations_base.BaseRelaxationData + The relaxation to be copied + old_var_to_new_var_map: dict + Map from the original variable id to the new variable + + Returns + ------- + rel: coramin.relaxations.relaxations_base.BaseRelaxationData + The copy of rel with new variables + """ + if isinstance(rel, PWXSquaredRelaxationData): + new_x = old_var_to_new_var_map[id(rel.get_rhs_vars()[0])] + new_aux_var = old_var_to_new_var_map[id(rel.get_aux_var())] + new_rel = PWXSquaredRelaxation(concrete=True) + new_rel.set_input(x=new_x, aux_var=new_aux_var, pw_repn=rel._pw_repn, + use_linear_relaxation=rel.use_linear_relaxation, + relaxation_side=rel.relaxation_side) + elif isinstance(rel, PWArctanRelaxationData): + new_x = old_var_to_new_var_map[id(rel.get_rhs_vars()[0])] + new_aux_var = old_var_to_new_var_map[id(rel.get_aux_var())] + new_rel = PWArctanRelaxation(concrete=True) + new_rel.set_input(x=new_x, aux_var=new_aux_var, pw_repn=rel._pw_repn, + relaxation_side=rel.relaxation_side, + use_linear_relaxation=rel.use_linear_relaxation) + elif isinstance(rel, PWSinRelaxationData): + new_x = old_var_to_new_var_map[id(rel.get_rhs_vars()[0])] + new_aux_var = old_var_to_new_var_map[id(rel.get_aux_var())] + new_rel = PWSinRelaxation(concrete=True) + new_rel.set_input(x=new_x, aux_var=new_aux_var, pw_repn=rel._pw_repn, + relaxation_side=rel.relaxation_side, + use_linear_relaxation=rel.use_linear_relaxation) + elif isinstance(rel, PWCosRelaxationData): + new_x = old_var_to_new_var_map[id(rel.get_rhs_vars()[0])] + new_aux_var = old_var_to_new_var_map[id(rel.get_aux_var())] + new_rel = PWCosRelaxation(concrete=True) + new_rel.set_input(x=new_x, aux_var=new_aux_var, pw_repn=rel._pw_repn, + relaxation_side=rel.relaxation_side, + use_linear_relaxation=rel.use_linear_relaxation) + elif isinstance(rel, PWUnivariateRelaxationData): + new_x = old_var_to_new_var_map[id(rel.get_rhs_vars()[0])] + new_aux_var = old_var_to_new_var_map[id(rel.get_aux_var())] + new_f_x_expr = replace_expressions(rel.get_rhs_expr(), + substitution_map=old_var_to_new_var_map, + remove_named_expressions=True) + new_rel = PWUnivariateRelaxation(concrete=True) + if rel.is_rhs_convex(): + shape = FunctionShape.CONVEX + elif rel.is_rhs_concave(): + shape = FunctionShape.CONCAVE + else: + shape = FunctionShape.UNKNOWN + new_rel.set_input(x=new_x, aux_var=new_aux_var, shape=shape, + f_x_expr=new_f_x_expr, pw_repn=rel._pw_repn, + relaxation_side=rel.relaxation_side, + use_linear_relaxation=rel.use_linear_relaxation) + elif isinstance(rel, PWMcCormickRelaxationData): + rhs_vars = rel.get_rhs_vars() + old_x1 = rhs_vars[0] + old_x2 = rhs_vars[1] + new_x1 = old_var_to_new_var_map[id(old_x1)] + new_x2 = old_var_to_new_var_map[id(old_x2)] + new_aux_var = old_var_to_new_var_map[id(rel.get_aux_var())] + new_rel = PWMcCormickRelaxation(concrete=True) + new_rel.set_input(x1=new_x1, x2=new_x2, aux_var=new_aux_var, + relaxation_side=rel.relaxation_side) + elif isinstance(rel, AlphaBBRelaxationData): + new_aux_var = old_var_to_new_var_map[id(rel.get_aux_var())] + new_f_x_expr = replace_expressions( + rel.get_rhs_expr(), + substitution_map=old_var_to_new_var_map, + remove_named_expressions=True + ) + new_rel = AlphaBBRelaxation(concrete=True) + new_rel.set_input( + aux_var=new_aux_var, + f_x_expr=new_f_x_expr, + relaxation_side=rel.relaxation_side, + use_linear_relaxation=rel.use_linear_relaxation, + eigenvalue_bounder=rel.hessian.method, + eigenvalue_opt=rel.hessian.opt, + ) + elif isinstance(rel, MultivariateRelaxationData): + new_aux_var = old_var_to_new_var_map[id(rel.get_aux_var())] + if rel.is_rhs_convex(): + shape = FunctionShape.CONVEX + elif rel.is_rhs_concave(): + shape = FunctionShape.CONCAVE + else: + shape = FunctionShape.UNKNOWN + new_f_x_expr = replace_expressions(rel.get_rhs_expr(), + substitution_map=old_var_to_new_var_map, + remove_named_expressions=True) + new_rel = MultivariateRelaxation(concrete=True) + new_rel.set_input(aux_var=new_aux_var, shape=shape, f_x_expr=new_f_x_expr, + use_linear_relaxation=rel.use_linear_relaxation) + else: + raise ValueError('Unrecognized relaxation: {0}'.format(str(type(rel)))) + + new_rel.small_coef = rel.small_coef + new_rel.large_coef = rel.large_coef + new_rel.safety_tol = rel.safety_tol + + return new_rel diff --git a/pyomo/contrib/coramin/relaxations/custom_block.py b/pyomo/contrib/coramin/relaxations/custom_block.py new file mode 100644 index 00000000000..9fed4e2f70c --- /dev/null +++ b/pyomo/contrib/coramin/relaxations/custom_block.py @@ -0,0 +1,83 @@ +import sys +from pyomo.core.base.block import Block +from pyomo.core.base.indexed_component import UnindexedComponent_set +# ToDo: documentation +# ToDo: passing of kwargs down to the data object +# ToDo: figure out if the setattr's are necessary in the decorator +# ToDo: decide if we need the decorator (it actually does not do much +# and can be replaced by one more class declaration that might be "easier" +# for the user anyway? +''' +This module implements meta classes and a decorator to make +it easier to create derived block types. With the decorator, +you only need to inherit from _BlockData. + +# ToDo: Document this custom block code with an example +''' + +class _IndexedCustomBlockMeta(type): + """Metaclass for creating an indexed block with + a custom block data type.""" + + def __new__(meta, name, bases, dct): + def __init__(self, *args, **kwargs): + bases[0].__init__(self, *args, **kwargs) + dct["__init__"] = __init__ + return type.__new__(meta, name, bases, dct) + +class _ScalarCustomBlockMeta(type): + '''Metaclass used to create a scalar block with a + custom block data type + ''' + def __new__(meta, name, bases, dct): + def __init__(self, *args, **kwargs): + # bases[0] is the custom block data object + bases[0].__init__(self, component=self) + # bases[1] is the custom block object that + # is used for declaration + bases[1].__init__(self, *args, **kwargs) + dct["__init__"] = __init__ + return type.__new__(meta, name, bases, dct) + +class CustomBlock(Block): + ''' This CustomBlock is the base class that allows + for easy creation of specialized derived blocks + ''' + def __new__(cls, *args, **kwds): + if cls.__name__.startswith('_Indexed') or \ + cls.__name__.startswith('_Scalar'): + # we are entering here the second time (recursive) + # therefore, we need to create what we have + return super(CustomBlock, cls).__new__(cls) + if not args or (args[0] is UnindexedComponent_set and len(args)==1): + bname = "_Scalar{}".format(cls.__name__) + n = _ScalarCustomBlockMeta(bname, (cls._ComponentDataClass, cls),{}) + return n.__new__(n) + else: + bname = "_Indexed{}".format(cls.__name__) + n = _IndexedCustomBlockMeta(bname, (cls,), {}) + return n.__new__(n) + + +def declare_custom_block(name): + ''' Decorator to declare the custom component + that goes along with a custom block data + + @declare_custom_block(name=FooBlock) + class FooBlockData(_BlockData): + # custom block data class + ''' + def proc_dec(cls): + # this is the decorator function that + # creates the block component class + c = type( + name, # name of new class + (CustomBlock,), # base classes + {"__module__": cls.__module__, "_ComponentDataClass": cls}) # magic to fix the module + + # are these necessary? + setattr(sys.modules[cls.__module__], name, c) + setattr(cls, '_orig_name', name) + setattr(cls, '_orig_module', cls.__module__) + return cls + return proc_dec diff --git a/pyomo/contrib/coramin/relaxations/hessian.py b/pyomo/contrib/coramin/relaxations/hessian.py new file mode 100644 index 00000000000..02d0f469de5 --- /dev/null +++ b/pyomo/contrib/coramin/relaxations/hessian.py @@ -0,0 +1,273 @@ +from pyomo.core.expr.calculus.diff_with_pyomo import reverse_sd +from pyomo.core.expr.visitor import identify_variables +from pyomo.common.collections import ComponentSet +import enum +import pyomo.environ as pe +from pyomo.core.expr.numvalue import is_fixed +import math +from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr +import numpy as np +from coramin.utils.coramin_enums import EigenValueBounder +from pyomo.core.base.block import _BlockData +from typing import Optional, MutableMapping +from pyomo.core.expr.numeric_expr import ExpressionBase +from pyomo.contrib import appsi +from pyomo.common.modeling import unique_component_name +from pyomo.core.base.var import _GeneralVarData +from coramin.utils.pyomo_utils import simplify_expr + + +def _2d_determinant(mat: np.ndarray): + return mat[0, 0] * mat[1, 1] - mat[1, 0] * mat[0, 1] + + +def _determinant(mat): + nrows, ncols = mat.shape + assert nrows == ncols + if nrows == 1: + det = mat[0, 0] + elif nrows == 2: + det = _2d_determinant(mat) + else: + i = 0 + det = 0 + next_rows = np.array(list(range(i+1, nrows)), dtype=int) + for j in range(nrows): + next_cols = [k for k in range(j)] + next_cols.extend(k for k in range(j+1, nrows)) + next_cols = np.array(next_cols, dtype=int) + next_mat = mat[next_rows, :] + next_mat = next_mat[:, next_cols] + det += (-1)**(i + j) * mat[i, j] * _determinant(next_mat) + return simplify_expr(det) + + +class Hessian(object): + def __init__( + self, + expr: ExpressionBase, + opt: Optional[appsi.base.Solver], + method: EigenValueBounder = EigenValueBounder.LinearProgram, + ): + self.method = EigenValueBounder(method) + self.opt = opt + self._expr = expr + self._var_list = list(identify_variables(expr=expr, include_fixed=False)) + self._ndx_map = pe.ComponentMap( + (v, ndx) for ndx, v in enumerate(self._var_list) + ) + self._hessian = self.compute_symbolic_hessian() + self._eigenvalue_problem: Optional[_BlockData] = None + self._eigenvalue_relaxation: Optional[_BlockData] = None + self._orig_to_relaxation_vars: Optional[ + MutableMapping[_GeneralVarData, _GeneralVarData] + ] = None + + def variables(self): + return tuple(self._var_list) + + def formulate_eigenvalue_problem(self, sense=pe.minimize): + if self._eigenvalue_problem is not None: + min_eig, max_eig = self.bound_eigenvalues_from_interval_hessian() + if min_eig > self._eigenvalue_problem.eig.lb: + self._eigenvalue_problem.eig.setlb(min_eig) + if max_eig < self._eigenvalue_problem.eig.ub: + self._eigenvalue_problem.eig.setub(max_eig) + self._eigenvalue_problem.obj.sense = sense + return self._eigenvalue_problem + min_eig, max_eig = self.bound_eigenvalues_from_interval_hessian() + m = pe.ConcreteModel() + m.eig = pe.Var(bounds=(min_eig, max_eig)) + m.obj = pe.Objective(expr=m.eig, sense=sense) + for v in self._var_list: + m.add_component(v.name, pe.Reference(v)) + + n = len(self._var_list) + np_hess = np.empty((n, n), dtype=object) + for ndx1, v1 in enumerate(self._var_list): + hess_v1 = self._hessian[v1] + for ndx2 in range(ndx1, n): + v2 = self._var_list[ndx2] + if v2 in hess_v1: + np_hess[ndx1, ndx2] = hess_v1[v2] + else: + np_hess[ndx1, ndx2] = 0 + if v1 is v2: + np_hess[ndx1, ndx2] -= m.eig + else: + np_hess[ndx2, ndx1] = np_hess[ndx1, ndx2] + m.det_con = pe.Constraint(expr=_determinant(np_hess) == 0) + self._eigenvalue_problem = m + return m + + def formulate_eigenvalue_relaxation(self, sense=pe.minimize): + if self._eigenvalue_relaxation is not None: + for orig_v, rel_v in self._orig_to_relaxation_vars.items(): + orig_lb, orig_ub = orig_v.bounds + rel_lb, rel_ub = rel_v.bounds + if orig_lb is not None: + if rel_lb is None or orig_lb > rel_lb: + rel_v.setlb(orig_lb) + if orig_ub is not None: + if rel_ub is None or orig_ub < rel_ub: + rel_v.setub(orig_ub) + from .iterators import relaxation_data_objects + for b in relaxation_data_objects(self._eigenvalue_relaxation, descend_into=True, active=True): + b.rebuild() + self._eigenvalue_relaxation.obj.sense = sense + return self._eigenvalue_relaxation + m = self.formulate_eigenvalue_problem(sense=sense) + all_vars = list( + ComponentSet( + m.component_data_objects(pe.Var, descend_into=True) + ) + ) + tmp_name = unique_component_name(m, "all_vars") + setattr(m, tmp_name, all_vars) + from .auto_relax import relax + relaxation = relax(m, in_place=False) + new_vars = getattr(relaxation, "all_vars") + self._orig_to_relaxation_vars = pe.ComponentMap(zip(all_vars, new_vars)) + delattr(m, tmp_name) + delattr(relaxation, tmp_name) + self._eigenvalue_relaxation = relaxation + return relaxation + + def get_minimum_eigenvalue(self): + if self.method <= EigenValueBounder.GershgorinWithSimplification: + res = self.bound_eigenvalues_from_interval_hessian()[0] + elif self.method == EigenValueBounder.LinearProgram: + m = self.formulate_eigenvalue_relaxation() + res = self.opt.solve(m).best_objective_bound + else: + m = self.formulate_eigenvalue_problem() + res = self.opt.solve(m).best_objective_bound + return res + + def get_maximum_eigenvalue(self): + if self.method <= EigenValueBounder.GershgorinWithSimplification: + res = self.bound_eigenvalues_from_interval_hessian()[1] + elif self.method == EigenValueBounder.LinearProgram: + m = self.formulate_eigenvalue_relaxation(sense=pe.maximize) + res = self.opt.solve(m).best_objective_bound + else: + m = self.formulate_eigenvalue_problem(sense=pe.maximize) + res = self.opt.solve(m).best_objective_bound + return res + + def bound_eigenvalues_from_interval_hessian(self): + ih = self.compute_interval_hessian() + min_eig = math.inf + max_eig = -math.inf + for v1 in self._var_list: + h = ih[v1] + if v1 in h: + row_min = h[v1][0] + row_max = h[v1][1] + else: + row_min = 0 + row_max = 0 + for v2, (lb, ub) in h.items(): + if v2 is v1: + continue + row_min -= max(abs(lb), abs(ub)) + row_max += max(abs(lb), abs(ub)) + min_eig = min(min_eig, row_min) + max_eig = max(max_eig, row_max) + return min_eig, max_eig + + def compute_symbolic_hessian(self): + ders = reverse_sd(self._expr) + ders2 = pe.ComponentMap() + for v in self._var_list: + ders2[v] = reverse_sd(ders[v]) + + res = pe.ComponentMap() + for v in self._var_list: + res[v] = pe.ComponentMap() + + n = len(self._var_list) + for v1 in self._var_list: + ndx1 = self._ndx_map[v1] + for ndx2 in range(ndx1, n): + v2 = self._var_list[ndx2] + if v2 not in ders2[v1]: + continue + der = ders2[v1][v2] + if is_fixed(der): + val = pe.value(der) + res[v1][v2] = val + else: + if self.method >= EigenValueBounder.GershgorinWithSimplification: + _der = simplify_expr(der) + else: + _der = der + res[v1][v2] = _der + res[v2][v1] = res[v1][v2] + + return res + + def compute_interval_hessian(self): + res = pe.ComponentMap() + for v in self._var_list: + res[v] = pe.ComponentMap() + + n = len(self._var_list) + for ndx1, v1 in enumerate(self._var_list): + for ndx2 in range(ndx1, n): + v2 = self._var_list[ndx2] + if v2 not in self._hessian[v1]: + continue + lb, ub = compute_bounds_on_expr(self._hessian[v1][v2]) + if lb is None: + lb = -math.inf + if ub is None: + ub = math.inf + res[v1][v2] = (lb, ub) + res[v2][v1] = (lb, ub) + return res + + def pprint(self, intervals=False): + if intervals: + ih = self.compute_interval_hessian() + else: + ih = self._hessian + n = len(self._var_list) + lens = np.ones((n, n), dtype=int) + strs = dict() + for ndx in range(n): + strs[ndx] = dict() + + for v1 in self._var_list: + ndx1 = self._ndx_map[v1] + for ndx2 in range(ndx1, n): + v2 = self._var_list[ndx2] + if v2 in ih[v1]: + der = ih[v1][v2] + else: + if intervals: + der = (0, 0) + else: + der = 0 + if intervals: + lb, ub = der + der_str = f"({lb:<.3f}, {ub:<.3f})" + else: + der_str = str(der) + strs[ndx1][ndx2] = der_str + strs[ndx2][ndx1] = der_str + lens[ndx1, ndx2] = len(der_str) + lens[ndx2, ndx1] = len(der_str) + + col_lens = np.max(lens, axis=0) + row_string = "" + for ndx, cl in enumerate(col_lens): + row_string += f"{{{ndx}:<{cl+2}}}" + + res = "" + for row_ndx in range(n): + row_entries = tuple(strs[row_ndx][i] for i in range(n)) + res += row_string.format(*row_entries) + res += '\n' + + print(res) diff --git a/pyomo/contrib/coramin/relaxations/iterators.py b/pyomo/contrib/coramin/relaxations/iterators.py new file mode 100644 index 00000000000..de797a307c4 --- /dev/null +++ b/pyomo/contrib/coramin/relaxations/iterators.py @@ -0,0 +1,95 @@ +import pyomo.environ as pe +from .relaxations_base import BaseRelaxationData +from typing import Generator + + +def relaxation_data_objects( + block, descend_into=True, active=None, sort=False +) -> Generator[BaseRelaxationData, None, None]: + """ + Iterate over all instances of BaseRelaxationData in the block. + + Parameters + ---------- + block: pyomo.core.base.block._BlockData + The Block in which to look for relaxations + descend_into: bool + Whether or not to look for relaxations in sub-blocks + active: bool + If True, then any relaxations that have been deactivated or live on deactivated blocks will not be returned. + sort: bool + + Returns + ------- + relaxations: generator + A generator yielding the relaxation objects. + """ + for b in block.component_data_objects( + pe.Block, descend_into=descend_into, active=active, sort=sort + ): + if isinstance(b, BaseRelaxationData): + yield b + + +def _nonrelaxation_block_objects(block, descend_into=True, active=None, sort=False): + for b in block.component_data_objects( + pe.Block, descend_into=False, active=active, sort=sort + ): + if isinstance(b, BaseRelaxationData): + continue + else: + yield b + if descend_into: + for _b in _nonrelaxation_block_objects( + b, descend_into=True, active=active, sort=sort + ): + yield _b + + +def nonrelaxation_component_data_objects( + block, ctype=None, active=None, sort=False, descend_into=True +): + """ + Iterate over all components with the corresponding ctype (e.g., Constraint) in the block excluding + those instances which are or live on relaxation objects (instances of BaseRelaxationData). + + Parameters + ---------- + block: pyomo.core.base.block._BlockData + The Block in which to look for components + ctype: type + The type of component to iterate over + descend_into: bool + Whether or not to look for components in sub-blocks + active: bool + If True, then any components that have been deactivated or live on deactivated blocks will not be returned. + sort: bool + + Returns + ------- + components: generator + A generator yielding the requested components. + """ + if not isinstance(ctype, type): + raise ValueError( + "nonrelaxation_component_data_objects expects ctype to be a type, not a " + + str(type(ctype)) + ) + if ctype is pe.Block: + for b in _nonrelaxation_block_objects( + block, descend_into=descend_into, active=active, sort=sort + ): + yield b + else: + for comp in block.component_data_objects( + ctype=ctype, descend_into=False, active=active, sort=sort + ): + yield comp + if descend_into: + for b in _nonrelaxation_block_objects( + block, descend_into=True, active=active, sort=sort + ): + for comp in b.component_data_objects( + ctype=ctype, descend_into=False, active=active, sort=sort + ): + yield comp diff --git a/pyomo/contrib/coramin/relaxations/mccormick.py b/pyomo/contrib/coramin/relaxations/mccormick.py new file mode 100644 index 00000000000..3b5aea907cb --- /dev/null +++ b/pyomo/contrib/coramin/relaxations/mccormick.py @@ -0,0 +1,307 @@ +import logging +import pyomo.environ as pyo +from coramin.utils.coramin_enums import RelaxationSide +from .custom_block import declare_custom_block +from .relaxations_base import BasePWRelaxationData, ComponentWeakRef, _check_cut +import math +from ._utils import check_var_pts, _get_bnds_list, _get_bnds_tuple +from pyomo.core.base.param import IndexedParam +from pyomo.core.base.constraint import IndexedConstraint +from pyomo.core.expr.numeric_expr import LinearExpression +from typing import Optional, Dict, Sequence +pe = pyo + +logger = logging.getLogger(__name__) + + +def _build_pw_mccormick_relaxation(b, x1, x2, aux_var, x1_pts, relaxation_side=RelaxationSide.BOTH, safety_tol=1e-10): + """ + This function creates piecewise envelopes to relax "aux_var = x1*x2". Note that the partitioning is done on "x1" only. + This is the "nf4r" from Gounaris, Misener, and Floudas (2009). + + Parameters + ---------- + b: pyo.ConcreteModel or pyo.Block + x1: pyomo.core.base.var._GeneralVarData + The "x1" variable in x1*x2 + x2: pyomo.core.base.var._GeneralVarData + The "x2" variable in x1*x2 + aux_var: pyomo.core.base.var._GeneralVarData + The "aux_var" variable that is replacing x*y + x1_pts: Sequence[float] + A list of floating point numbers to define the points over which the piecewise representation will generated. + This list must be ordered, and it is expected that the first point (x_pts[0]) is equal to x.lb and the + last point (x_pts[-1]) is equal to x.ub + relaxation_side : minlp.minlp_defn.RelaxationSide + Provide the desired side for the relaxation (OVER, UNDER, or BOTH) + """ + assert len(x1_pts) > 2 + + x1_lb = x1_pts[0] + x1_ub = x1_pts[-1] + x2_lb, x2_ub = tuple(_get_bnds_list(x2)) + + check_var_pts(x1, x_pts=x1_pts) + check_var_pts(x2) + + if x1.is_fixed() and x2.is_fixed(): + b.x1_x2_fixed_eq = pyo.Constraint(expr= aux_var == pyo.value(x1) * pyo.value(x2)) + elif x1.is_fixed(): + b.x1_fixed_eq = pyo.Constraint(expr= aux_var == pyo.value(x1) * x2) + elif x2.is_fixed(): + b.x2_fixed_eq = pyo.Constraint(expr= aux_var == x1 * pyo.value(x2)) + else: + # create the lambda_ variables (binaries for the pw representation) + b.interval_set = pyo.Set(initialize=range(1, len(x1_pts))) + b.lambda_ = pyo.Var(b.interval_set, within=pyo.Binary) + + # create the delta x2 variables + b.delta_x2 = pyo.Var(b.interval_set, bounds=(0, None)) + + # create the "sos1" constraint + b.lambda_sos1 = pyo.Constraint(expr=sum(b.lambda_[n] for n in b.interval_set) == 1.0) + + # create the x1 interval constraints + b.x1_interval_lb = pyo.Constraint(expr=sum(x1_pts[n - 1] * b.lambda_[n] for n in b.interval_set) <= x1) + b.x1_interval_ub = pyo.Constraint(expr=x1 <= sum(x1_pts[n] * b.lambda_[n] for n in b.interval_set)) + + # create the x2 constraints + b.x2_con = pyo.Constraint(expr=x2 == x2_lb + sum(b.delta_x2[n] for n in b.interval_set)) + + def delta_x2n_ub_rule(m, n): + return b.delta_x2[n] <= (x2_ub - x2_lb) * b.lambda_[n] + + b.delta_x2n_ub = pyo.Constraint(b.interval_set, rule=delta_x2n_ub_rule) + + # create the relaxation constraints + if relaxation_side == RelaxationSide.UNDER or relaxation_side == RelaxationSide.BOTH: + b.aux_var_lb1 = pyo.Constraint(expr=(aux_var >= x2_ub * x1 + sum(x1_pts[n] * b.delta_x2[n] for n in b.interval_set) - + (x2_ub - x2_lb) * sum(x1_pts[n] * b.lambda_[n] for n in b.interval_set) - safety_tol)) + b.aux_var_lb2 = pyo.Constraint(expr=aux_var >= x2_lb * x1 + sum(x1_pts[n - 1] * b.delta_x2[n] for n in b.interval_set) - safety_tol) + + if relaxation_side == RelaxationSide.OVER or relaxation_side == RelaxationSide.BOTH: + b.aux_var_ub1 = pyo.Constraint(expr=(aux_var <= x2_ub * x1 + sum(x1_pts[n - 1] * b.delta_x2[n] for n in b.interval_set) - + (x2_ub - x2_lb) * sum(x1_pts[n - 1] * b.lambda_[n] for n in b.interval_set) + safety_tol)) + b.aux_var_ub2 = pyo.Constraint(expr=aux_var <= x2_lb * x1 + sum(x1_pts[n] * b.delta_x2[n] for n in b.interval_set) + safety_tol) + + +@declare_custom_block(name='PWMcCormickRelaxation') +class PWMcCormickRelaxationData(BasePWRelaxationData): + """ + A class for managing McCormick relaxations of bilinear terms (aux_var = x1 * x2). + """ + + def __init__(self, component): + BasePWRelaxationData.__init__(self, component) + self._x1ref = ComponentWeakRef(None) + self._x2ref = ComponentWeakRef(None) + self._aux_var_ref = ComponentWeakRef(None) + self._f_x_expr = None + self._mc_index = None + self._slopes_index = None + self._v_index = None + self._slopes: Optional[IndexedParam] = None + self._intercepts: Optional[IndexedParam] = None + self._mccormicks: Optional[IndexedConstraint] = None + self._mc_exprs: Dict[int, LinearExpression] = dict() + self._pw = None + + @property + def _x1(self): + return self._x1ref.get_component() + + @property + def _x2(self): + return self._x2ref.get_component() + + @property + def _aux_var(self): + return self._aux_var_ref.get_component() + + def get_rhs_vars(self): + return self._x1, self._x2 + + def get_rhs_expr(self): + return self._f_x_expr + + def vars_with_bounds_in_relaxation(self): + return [self._x1, self._x2] + + def _remove_relaxation(self): + del self._slopes, self._intercepts, self._mccormicks, self._pw, \ + self._mc_index, self._v_index, self._slopes_index + self._mc_index = None + self._v_index = None + self._slopes_index = None + self._slopes = None + self._intercepts = None + self._mccormicks = None + self._mc_exprs = dict() + self._pw = None + + def set_input(self, x1, x2, aux_var, relaxation_side=RelaxationSide.BOTH, large_coef=1e5, small_coef=1e-10, + safety_tol=1e-10): + """ + Parameters + ---------- + x1 : pyomo.core.base.var._GeneralVarData + The "x1" variable in x1*x2 + x2 : pyomo.core.base.var._GeneralVarData + The "x2" variable in x1*x2 + aux_var : pyomo.core.base.var._GeneralVarData + The "aux_var" auxillary variable that is replacing x1*x2 + relaxation_side : minlp.minlp_defn.RelaxationSide + Provide the desired side for the relaxation (OVER, UNDER, or BOTH) + """ + super(PWMcCormickRelaxationData, self).set_input(relaxation_side=relaxation_side, + use_linear_relaxation=True, + large_coef=large_coef, small_coef=small_coef, + safety_tol=safety_tol) + self._x1ref.set_component(x1) + self._x2ref.set_component(x2) + self._aux_var_ref.set_component(aux_var) + self._partitions[self._x1] = _get_bnds_list(self._x1) + self._f_x_expr = x1 * x2 + + def build(self, x1, x2, aux_var, relaxation_side=RelaxationSide.BOTH, large_coef=1e5, small_coef=1e-10, + safety_tol=1e-10): + """ + Parameters + ---------- + x1 : pyomo.core.base.var._GeneralVarData + The "x1" variable in x1*x2 + x2 : pyomo.core.base.var._GeneralVarData + The "x2" variable in x1*x2 + aux_var : pyomo.core.base.var._GeneralVarData + The "aux_var" auxillary variable that is replacing x1*x2 + relaxation_side : minlp.minlp_defn.RelaxationSide + Provide the desired side for the relaxation (OVER, UNDER, or BOTH) + """ + self.set_input(x1=x1, x2=x2, aux_var=aux_var, relaxation_side=relaxation_side, + large_coef=large_coef, small_coef=small_coef, safety_tol=safety_tol) + self.rebuild() + + def remove_relaxation(self): + super(PWMcCormickRelaxationData, self).remove_relaxation() + self._remove_relaxation() + + def rebuild(self, build_nonlinear_constraint=False, ensure_oa_at_vertices=True): + super(PWMcCormickRelaxationData, self).rebuild(build_nonlinear_constraint=build_nonlinear_constraint, + ensure_oa_at_vertices=ensure_oa_at_vertices) + if not build_nonlinear_constraint: + if self._check_valid_domain_for_relaxation(): + if len(self._partitions[self._x1]) == 2: + if self._mccormicks is None: + self._remove_relaxation() + self._build_mccormicks() + self._update_mccormicks() + else: + self._remove_relaxation() + del self._pw + self._pw = pe.Block(concrete=True) + _build_pw_mccormick_relaxation(b=self._pw, x1=self._x1, x2=self._x2, aux_var=self._aux_var, + x1_pts=self._partitions[self._x1], + relaxation_side=self.relaxation_side, safety_tol=self.safety_tol) + else: + self._remove_relaxation() + + def _build_mccormicks(self): + del self._mc_index, self._v_index, self._slopes_index, self._slopes, self._intercepts, self._mccormicks + self._mc_exprs = dict() + self._mc_index = pe.Set(initialize=[0, 1, 2, 3]) + self._v_index = pe.Set(initialize=[1, 2]) + self._slopes_index = pe.Set(initialize=self._mc_index * self._v_index) + self._slopes = IndexedParam(self._slopes_index, mutable=True) + self._intercepts = IndexedParam(self._mc_index, mutable=True) + self._mccormicks = IndexedConstraint(self._mc_index) + + if self.relaxation_side in {RelaxationSide.BOTH, RelaxationSide.UNDER}: + for ndx in [0, 1]: + e = LinearExpression(constant=self._intercepts[ndx], + linear_coefs=[self._slopes[ndx, 1], self._slopes[ndx, 2]], + linear_vars=[self._x1, self._x2]) + self._mc_exprs[ndx] = e + self._mccormicks[ndx] = self._aux_var >= e + + if self.relaxation_side in {RelaxationSide.BOTH, RelaxationSide.OVER}: + for ndx in [2, 3]: + e = LinearExpression(constant=self._intercepts[ndx], + linear_coefs=[self._slopes[ndx, 1], self._slopes[ndx, 2]], + linear_vars=[self._x1, self._x2]) + self._mc_exprs[ndx] = e + self._mccormicks[ndx] = self._aux_var <= e + + def _check_expr(self, ndx): + if ndx in {0, 1}: + rel_side = RelaxationSide.UNDER + else: + rel_side = RelaxationSide.OVER + success, bad_var, bad_coef, err_msg = _check_cut(self._mc_exprs[ndx], too_small=self.small_coef, + too_large=self.large_coef, relaxation_side=rel_side, + safety_tol=self.safety_tol) + if not success: + self._log_bad_cut(bad_var, bad_coef, err_msg) + self._mccormicks[ndx].deactivate() + else: + self._mccormicks[ndx].activate() + + def _update_mccormicks(self): + x1_lb, x1_ub = _get_bnds_tuple(self._x1) + x2_lb, x2_ub = _get_bnds_tuple(self._x2) + + if self.relaxation_side in {RelaxationSide.BOTH, RelaxationSide.UNDER}: + self._slopes[0, 1]._value = x2_lb + self._slopes[0, 2]._value = x1_lb + self._intercepts[0]._value = -x1_lb * x2_lb + + self._slopes[1, 1]._value = x2_ub + self._slopes[1, 2]._value = x1_ub + self._intercepts[1]._value = -x1_ub * x2_ub + + self._check_expr(0) + self._check_expr(1) + + if self.relaxation_side in {RelaxationSide.BOTH, RelaxationSide.OVER}: + self._slopes[2, 1]._value = x2_lb + self._slopes[2, 2]._value = x1_ub + self._intercepts[2]._value = -x1_ub * x2_lb + + self._slopes[3, 1]._value = x2_ub + self._slopes[3, 2]._value = x1_lb + self._intercepts[3]._value = -x1_lb * x2_ub + + self._check_expr(2) + self._check_expr(3) + + def add_partition_point(self, value=None): + """ + This method adds one point to the partitioning of x1. If value is not + specified, a single point will be added to the partitioning of x1 at the current value of x1. If value is + specified, then value is added to the partitioning of x1. + + Parameters + ---------- + value: float + The point to be added to the partitioning of x1. + """ + self._add_partition_point(self._x1, value) + + def is_rhs_convex(self): + """ + Returns True if linear underestimators do not need binaries. Otherwise, returns False. + + Returns + ------- + bool + """ + return False + + def is_rhs_concave(self): + """ + Returns True if linear overestimators do not need binaries. Otherwise, returns False. + + Returns + ------- + bool + """ + return False diff --git a/pyomo/contrib/coramin/relaxations/multivariate.py b/pyomo/contrib/coramin/relaxations/multivariate.py new file mode 100644 index 00000000000..895a464a4a5 --- /dev/null +++ b/pyomo/contrib/coramin/relaxations/multivariate.py @@ -0,0 +1,93 @@ +from coramin.utils.coramin_enums import RelaxationSide, FunctionShape +from coramin.relaxations.custom_block import declare_custom_block +from coramin.relaxations.relaxations_base import BaseRelaxationData, ComponentWeakRef +from pyomo.core.expr.visitor import identify_variables +import math +import pyomo.environ as pe +from coramin.relaxations._utils import _get_bnds_list + + +@declare_custom_block(name='MultivariateRelaxation') +class MultivariateRelaxationData(BaseRelaxationData): + def __init__(self, component): + super(MultivariateRelaxationData, self).__init__(component) + self._xs = None + self._aux_var_ref = ComponentWeakRef(None) + self._f_x_expr = None + self._function_shape = FunctionShape.UNKNOWN + + @property + def _aux_var(self): + return self._aux_var_ref.get_component() + + def get_rhs_vars(self): + return self._xs + + def get_rhs_expr(self): + return self._f_x_expr + + def vars_with_bounds_in_relaxation(self): + return list() + + def set_input(self, aux_var, shape, f_x_expr, use_linear_relaxation=True, large_coef=1e5, small_coef=1e-10, + safety_tol=1e-10): + """ + Parameters + ---------- + aux_var: pyomo.core.base.var._GeneralVarData + The auxiliary variable replacing f(x) + shape: FunctionShape + Either FunctionShape.CONVEX or FunctionShape.CONCAVE + f_x_expr: pyomo expression + The pyomo expression representing f(x) + use_linear_relaxation: bool + Specifies whether a linear or nonlinear relaxation should be used + """ + if shape not in {FunctionShape.CONVEX, FunctionShape.CONCAVE}: + raise ValueError('MultivariateRelaxation only supports concave or convex functions.') + self._function_shape = shape + if shape == FunctionShape.CONVEX: + relaxation_side = RelaxationSide.UNDER + else: + relaxation_side = RelaxationSide.OVER + super().set_input(relaxation_side=relaxation_side, + use_linear_relaxation=use_linear_relaxation, + large_coef=large_coef, small_coef=small_coef, + safety_tol=safety_tol) + self._xs = tuple(identify_variables(f_x_expr, include_fixed=False)) + self._aux_var_ref.set_component(aux_var) + self._f_x_expr = f_x_expr + + def build(self, aux_var, shape, f_x_expr, use_linear_relaxation=True, large_coef=1e5, small_coef=1e-10, + safety_tol=1e-10): + self.set_input(aux_var=aux_var, shape=shape, f_x_expr=f_x_expr, use_linear_relaxation=use_linear_relaxation, + large_coef=large_coef, small_coef=small_coef, safety_tol=safety_tol) + self.rebuild() + + def is_rhs_convex(self): + return self._function_shape == FunctionShape.CONVEX + + def is_rhs_concave(self): + return self._function_shape == FunctionShape.CONCAVE + + @property + def use_linear_relaxation(self): + return self._use_linear_relaxation + + @use_linear_relaxation.setter + def use_linear_relaxation(self, value): + self._use_linear_relaxation = value + + @property + def relaxation_side(self): + return BaseRelaxationData.relaxation_side.fget(self) + + @relaxation_side.setter + def relaxation_side(self, val): + if self.is_rhs_convex(): + if val != RelaxationSide.UNDER: + raise ValueError('MultivariateRelaxations only support underestimators for convex functions') + if self.is_rhs_concave(): + if val != RelaxationSide.OVER: + raise ValueError('MultivariateRelaxations only support overestimators for concave functions') + BaseRelaxationData.relaxation_side.fset(self, val) diff --git a/pyomo/contrib/coramin/relaxations/relaxations_base.py b/pyomo/contrib/coramin/relaxations/relaxations_base.py new file mode 100644 index 00000000000..da74db9cbdd --- /dev/null +++ b/pyomo/contrib/coramin/relaxations/relaxations_base.py @@ -0,0 +1,746 @@ +from pyomo.core.base.block import _BlockData, Block +from .custom_block import declare_custom_block +import weakref +import pyomo.environ as pe +from collections.abc import Iterable +from pyomo.common.collections import ComponentSet, ComponentMap +from coramin.utils.coramin_enums import FunctionShape, RelaxationSide +import warnings +import logging +import math +from ._utils import _get_bnds_list, _get_bnds_tuple +import sys +from pyomo.core.expr import taylor_series_expansion +from typing import Sequence, Dict, Tuple, Optional, Union, Mapping, MutableMapping, List +from pyomo.core.base.param import IndexedParam, ScalarParam, _ParamData +from pyomo.core.base.var import _GeneralVarData +from pyomo.core.expr.calculus.diff_with_pyomo import reverse_sd +from pyomo.core.expr.numeric_expr import LinearExpression, ExpressionBase +from pyomo.core.base.constraint import IndexedConstraint, ScalarConstraint, _GeneralConstraintData +from pyomo.contrib.fbbt import interval + +pyo = pe +logger = logging.getLogger(__name__) + +""" +Base classes for relaxations +""" + + +class _OACut(object): + def __init__(self, + nonlin_expr, + expr_vars: Sequence[_GeneralVarData], + coefficients: Sequence[_ParamData], + offset: _ParamData): + self.expr_vars = expr_vars + self.nonlin_expr = nonlin_expr + self.coefficients = coefficients + self.offset = offset + derivs = reverse_sd(self.nonlin_expr) + self.derivs = [derivs[i] for i in self.expr_vars] + self.cut_expr = LinearExpression(constant=self.offset, + linear_coefs=self.coefficients, + linear_vars=self.expr_vars) + self.current_pt = None + + def update(self, + var_vals: Sequence[float], + relaxation_side: RelaxationSide, + too_small: float, + too_large: float, + safety_tol: float) -> Tuple[bool, Optional[_GeneralVarData], Optional[float], Optional[str]]: + res = (True, None, None, None) + self.current_pt = var_vals + orig_values = [i.value for i in self.expr_vars] + for v, val in zip(self.expr_vars, var_vals): + v.set_value(val, skip_validation=True) + try: + offset_val = pe.value(self.nonlin_expr) + for ndx, v in enumerate(self.expr_vars): + der = pe.value(self.derivs[ndx]) + offset_val -= der * v.value + self.coefficients[ndx]._value = der + self.offset._value = offset_val + except (OverflowError, ValueError, ZeroDivisionError) as e: + res = (False, None, None, str(e)) + finally: + for v, val in zip(self.expr_vars, orig_values): + v.set_value(val, skip_validation=True) + if res[0]: + res = _check_cut(self.cut_expr, too_small=too_small, too_large=too_large, relaxation_side=relaxation_side, + safety_tol=safety_tol) + return res + + def __repr__(self): + pt_str = {str(v): p for v, p in zip(self.expr_vars, self.current_pt)} + pt_str = str(pt_str) + s = f'OA Cut at {pt_str}' + return s + + def __str__(self): + return self.__repr__() + + +def _check_cut(cut: LinearExpression, too_small, too_large, relaxation_side, safety_tol): + res = (True, None, None, None) + for coef_p, v in zip(cut.linear_coefs, cut.linear_vars): + coef = coef_p.value + if not math.isfinite(coef) or abs(coef) >= too_large: + res = (False, v, coef, None) + elif 0 < abs(coef) <= too_small and v.has_lb() and v.has_ub(): + coef_p._value = 0 + if relaxation_side == RelaxationSide.UNDER: + cut.constant._value = interval.add(cut.constant.value, cut.constant.value, + *interval.mul(v.lb, v.ub, coef, coef))[0] + elif relaxation_side == RelaxationSide.OVER: + cut.constant._value = interval.add(cut.constant.value, cut.constant.value, + *interval.mul(v.lb, v.ub, coef, coef))[1] + else: + raise ValueError('relaxation_side should be either UNDER or OVER') + if relaxation_side == RelaxationSide.UNDER: + cut.constant._value -= safety_tol + else: + cut.constant._value += safety_tol + if not math.isfinite(cut.constant.value) or abs(cut.constant.value) >= too_large: + res = (False, None, cut.constant.value, None) + return res + + +@declare_custom_block(name='BaseRelaxation') +class BaseRelaxationData(_BlockData): + def __init__(self, component): + _BlockData.__init__(self, component) + self._relaxation_side = RelaxationSide.BOTH + self._use_linear_relaxation = True + self._large_coef = 1e5 + self._small_coef = 1e-10 + self._needs_rebuilt = True + self.safety_tol = 1e-10 + + self._oa_points: Dict[Tuple[float, ...], _OACut] = dict() + self._oa_param_indices: MutableMapping[_ParamData, int] = pe.ComponentMap() + self._current_param_index = 0 + self._oa_params: Optional[IndexedParam] = None + self._cuts: Optional[IndexedConstraint] = None + + self._saved_oa_points = list() + self._oa_stack_map = dict() + + self._original_constraint: Optional[ScalarConstraint] = None + self._nonlinear: Optional[ScalarConstraint] = None + + def set_input(self, relaxation_side=RelaxationSide.BOTH, + use_linear_relaxation=True, large_coef=1e5, + small_coef=1e-10, safety_tol=1e-10): + self.relaxation_side = relaxation_side + self.use_linear_relaxation = use_linear_relaxation + self._large_coef = large_coef + self._small_coef = small_coef + self.safety_tol = safety_tol + self._needs_rebuilt = True + + self.clear_oa_points() + self._saved_oa_points = list() + self._oa_stack_map = dict() + + def get_aux_var(self) -> _GeneralVarData: + """ + All Coramin relaxations are relaxations of constraints of the form w <=/=/>= f(x). This method returns w + + Returns + ------- + aux_var: pyomo.core.base.var._GeneralVarData + The variable representing w in w = f(x) (which is the constraint being relaxed). + """ + return self._aux_var + + def get_rhs_vars(self) -> Tuple[_GeneralVarData, ...]: + raise NotImplementedError('This method should be implemented by subclasses') + + def get_rhs_expr(self) -> ExpressionBase: + raise NotImplementedError('This method should be implemented by subclasses') + + def _get_expr_for_oa(self): + return self.get_rhs_expr() + + @property + def small_coef(self): + return self._small_coef + + @small_coef.setter + def small_coef(self, val): + self._small_coef = val + + @property + def large_coef(self): + return self._large_coef + + @large_coef.setter + def large_coef(self, val): + self._large_coef = val + + @property + def use_linear_relaxation(self) -> bool: + """ + If this is True, the relaxation will use a linear relaxation. If False, then a nonlinear relaxation may be used. + Take x^2 for example, the underestimator can be quadratic. + + Returns + ------- + bool + """ + return self._use_linear_relaxation + + @use_linear_relaxation.setter + def use_linear_relaxation(self, val: bool): + if not val: + raise ValueError('Relaxations of type {0} do not support relaxations that are not linear.'.format(type(self))) + + def remove_relaxation(self): + """ + Remove any auto-created vars/constraints from the relaxation block + """ + del self._cuts + self._cuts = None + del self._original_constraint + self._original_constraint = None + del self._nonlinear + self._nonlinear = None + + def _has_a_convex_side(self): + if self.has_convex_underestimator() and self.relaxation_side in {RelaxationSide.UNDER, RelaxationSide.BOTH}: + return True + if self.has_concave_overestimator() and self.relaxation_side in {RelaxationSide.OVER, RelaxationSide.BOTH}: + return True + return False + + def _check_valid_domain_for_relaxation(self) -> bool: + for v in self.get_rhs_vars(): + lb, ub = _get_bnds_tuple(v) + if not math.isfinite(lb) or not math.isfinite(ub): + return False + return True + + def rebuild(self, build_nonlinear_constraint=False, ensure_oa_at_vertices=True): + # we have to ensure only one of + # - self._cuts + # - self._nonlinear + # - self._original_constraint + # is ever not None at one time + needs_rebuilt = self._needs_rebuilt + if build_nonlinear_constraint: + if self._original_constraint is None: + needs_rebuilt = True + else: + if self.use_linear_relaxation: + if self._nonlinear is not None or self._original_constraint is not None: + needs_rebuilt = True + else: + if self._cuts is not None or self._original_constraint is not None: + needs_rebuilt = True + + if needs_rebuilt: + self.remove_relaxation() + + self._needs_rebuilt = False + + if build_nonlinear_constraint and self._original_constraint is None: + del self._original_constraint + if self.relaxation_side == RelaxationSide.BOTH: + self._original_constraint = pe.Constraint(expr=self.get_aux_var() == self.get_rhs_expr()) + elif self.relaxation_side == RelaxationSide.UNDER: + self._original_constraint = pe.Constraint(expr=self.get_aux_var() >= self.get_rhs_expr()) + else: + self._original_constraint = pe.Constraint(expr=self.get_aux_var() <= self.get_rhs_expr()) + else: + if self._has_a_convex_side(): + if self.use_linear_relaxation: + if self._cuts is None: + del self._cuts + self._cuts = IndexedConstraint(pe.Any) + if self._oa_params is None: + del self._oa_params + self._oa_params = IndexedParam(pe.Any, mutable=True) + self.clean_oa_points(ensure_oa_at_vertices=ensure_oa_at_vertices) + self._update_oa_cuts() + else: + if self._nonlinear is None: + del self._nonlinear + if self.has_convex_underestimator(): + self._nonlinear = pe.Constraint(expr=self.get_aux_var() >= self._get_expr_for_oa() - self.safety_tol) + else: + assert self.has_concave_overestimator() + self._nonlinear = pe.Constraint(expr=self.get_aux_var() <= self._get_expr_for_oa() + self.safety_tol) + + def vars_with_bounds_in_relaxation(self): + """ + This method returns a list of variables whose bounds appear in the constraints defining the relaxation. + Take the McCormick relaxation of a bilinear term (w = x * y) for example. The McCormick relaxation is + + w >= xl * y + x * yl - xl * yl + w >= xu * y + x * yu - xu * yu + w <= xu * y + x * yl - xu * yl + w <= x * yu + xl * y - xl * yu + + where xl and xu are the lower and upper bounds for x, respectively, and yl and yu are the lower and upper + bounds for y, respectively. Because xl, xu, yl, and yu appear in the constraints, this method would return + + [x, y] + + As another example, take w >= x**2. A linear relaxation of this constraint just involves linear underestimators, + which do not depend on the bounds of x or w. Therefore, this method would return an empty list. + """ + raise NotImplementedError('This method should be implemented in the derived class.') + + def get_deviation(self): + """ + All Coramin relaxations are relaxations of constraints of the form w <=/=/>= f(x). This method returns + + max{f(x) - w, 0} if relaxation_side is RelaxationSide.UNDER + max{w - f(x), 0} if relaxation_side is RelaxationSide.OVER + abs(w - f(x)) if relaxation_side is RelaxationSide.BOTH + + Returns + ------- + float + """ + dev = self.get_aux_var().value - pe.value(self.get_rhs_expr()) + if self.relaxation_side is RelaxationSide.BOTH: + dev = abs(dev) + elif self.relaxation_side is RelaxationSide.UNDER: + dev = max(-dev, 0) + else: + dev = max(dev, 0) + return dev + + def is_rhs_convex(self): + """ + All Coramin relaxations are relaxations of constraints of the form w <=/=/>= f(x). This method returns True if f(x) + is convex and False otherwise. + + Returns + ------- + bool + """ + raise NotImplementedError('This method should be implemented in the derived class.') + + def is_rhs_concave(self): + """ + All Coramin relaxations are relaxations of constraints of the form w <=/=/>= f(x). This method returns True if f(x) + is concave and False otherwise. + + Returns + ------- + bool + """ + raise NotImplementedError('This method should be implemented in the derived class.') + + def has_convex_underestimator(self): + return self.is_rhs_convex() + + def has_concave_overestimator(self): + return self.is_rhs_concave() + + @property + def relaxation_side(self): + return self._relaxation_side + + @relaxation_side.setter + def relaxation_side(self, val): + if val not in RelaxationSide: + raise ValueError('{0} is not a valid member of RelaxationSide'.format(val)) + if val != self._relaxation_side: + self._needs_rebuilt = True + self._relaxation_side = val + + def _get_pprint_string(self): + if self.relaxation_side == RelaxationSide.BOTH: + relational_operator_string = '==' + elif self.relaxation_side == RelaxationSide.UNDER: + relational_operator_string = '>=' + elif self.relaxation_side == RelaxationSide.OVER: + relational_operator_string = '<=' + else: + raise ValueError('Unexpected relaxation side') + return f'Relaxation for {self.get_aux_var().name} {relational_operator_string} {str(self.get_rhs_expr())}' + + def pprint(self, ostream=None, verbose=False, prefix=""): + if ostream is None: + ostream = sys.stdout + + ostream.write('{0}{1}: {2}\n'.format(prefix, self.name, self._get_pprint_string())) + + if verbose: + super(BaseRelaxationData, self).pprint(ostream=ostream, + verbose=verbose, prefix=(prefix + ' ')) + + def _get_oa_cut(self) -> _OACut: + rhs_vars = self.get_rhs_vars() + coef_params = list() + for v in rhs_vars: + p = self._oa_params[self._current_param_index] + self._oa_param_indices[p] = self._current_param_index + coef_params.append(p) + self._current_param_index += 1 + offset_param = self._oa_params[self._current_param_index] + self._oa_param_indices[offset_param] = self._current_param_index + self._current_param_index += 1 + oa_cut = _OACut(self._get_expr_for_oa(), rhs_vars, coef_params, offset_param) + return oa_cut + + def _remove_oa_cut(self, oa_cut: _OACut): + for p in oa_cut.coefficients: + del self._oa_params[self._oa_param_indices[p]] + del self._oa_param_indices[p] + del self._oa_params[self._oa_param_indices[oa_cut.offset]] + del self._oa_param_indices[oa_cut.offset] + if oa_cut in self._cuts: # if the cut did not pass _check_cut, it won't be in self._cuts + del self._cuts[oa_cut] + + def _log_bad_cut(self, fail_var, fail_coef, err_msg): + if fail_var is None and fail_coef is None: + logger.debug(f'Encountered exception when adding OA cut ' + f'for "{self._get_pprint_string()}"; Error message: {err_msg}') + elif fail_var is None: + logger.debug(f'Skipped OA cut for "{self._get_pprint_string()}" due to a ' + f'large constant value: {fail_coef}') + else: + logger.debug(f'Skipped OA cut for "{self._get_pprint_string()}" due to a ' + f'small or large coefficient for {str(fail_var)}: {fail_coef}') + + def _add_oa_cut(self, pt_tuple: Tuple[float, ...], oa_cut: _OACut) -> Optional[_GeneralConstraintData]: + if self._nonlinear is not None or self._original_constraint is not None: + raise ValueError('Can only add an OA cut when using a linear relaxation') + if self.has_convex_underestimator(): + rel_side = RelaxationSide.UNDER + else: + assert self.has_concave_overestimator() + rel_side = RelaxationSide.OVER + cut_info = oa_cut.update(var_vals=pt_tuple, relaxation_side=rel_side, + too_small=self.small_coef, too_large=self.large_coef, + safety_tol=self.safety_tol) + success, fail_var, fail_coef, err_msg = cut_info + if not success: + self._log_bad_cut(fail_var, fail_coef, err_msg) + if oa_cut in self._cuts: + del self._cuts[oa_cut] + else: + if oa_cut not in self._cuts: + if self.has_convex_underestimator(): + self._cuts[oa_cut] = self.get_aux_var() >= oa_cut.cut_expr + else: + self._cuts[oa_cut] = self.get_aux_var() <= oa_cut.cut_expr + return self._cuts[oa_cut] + return None + + def _update_oa_cuts(self): + for pt_tuple, oa_cut in self._oa_points.items(): + self._add_oa_cut(pt_tuple, oa_cut) + + # remove any cuts that may have been added with add_cut(keep_cut=False) + all_oa_cuts = set(self._oa_points.values()) + for oa_cut in list(self._cuts): + if oa_cut not in all_oa_cuts: + self._remove_oa_cut(oa_cut) + + def _add_oa_point(self, pt_tuple: Tuple[float, ...]): + if pt_tuple not in self._oa_points: + self._oa_points[pt_tuple] = self._get_oa_cut() + + def add_oa_point(self, var_values: Optional[Union[Tuple[float, ...], Mapping[_GeneralVarData, float]]] = None): + """ + Add a point at which an outer-approximation cut for a convex constraint should be added. This does not + rebuild the relaxation. You must call rebuild() for the constraint to get added. + + Parameters + ---------- + var_values: Optional[Union[Tuple[float, ...], Mapping[_GeneralVarData, float]]] + """ + if self._has_a_convex_side(): + if var_values is None: + var_values = tuple(v.value for v in self.get_rhs_vars()) + elif type(var_values) is tuple: + pass + else: + var_values = tuple(var_values[v] for v in self.get_rhs_vars()) + self._add_oa_point(var_values) + + def push_oa_points(self, key=None): + """ + Save the current list of OA points for later use through pop_oa_points(). + """ + to_save = [i for i in self._oa_points.keys()] + if key is not None: + self._oa_stack_map[key] = to_save + else: + self._saved_oa_points.append(to_save) + + def clear_oa_points(self): + """ + Delete any existing OA points. + """ + self._oa_points = dict() + self._oa_param_indices = pe.ComponentMap() + self._current_param_index = 0 + if self._oa_params is not None: + del self._oa_params + self._oa_params = pe.Param(pe.Any, mutable=True) + if self._cuts is not None: + del self._cuts + self._cuts = pe.Constraint(pe.Any) + + def pop_oa_points(self, key=None): + """ + Use the most recently saved list of OA points + """ + self.clear_oa_points() + if key is None: + list_of_points = self._saved_oa_points.pop(-1) + else: + list_of_points = self._oa_stack_map.pop(key) + for pt_tuple in list_of_points: + self._add_oa_point(pt_tuple) + + def add_cut(self, keep_cut=True, check_violation=True, feasibility_tol=1e-8) -> Optional[_GeneralConstraintData]: + """ + This function will add a linear cut to the relaxation. Cuts are only generated for the convex side of the + constraint (if the constraint has a convex side). For example, if the relaxation is a PWXSquaredRelaxationData + for y = x**2, the add_cut will add an underestimator at x.value (but only if y.value < x.value**2). If + relaxation is a PWXSquaredRelaxationData for y < x**2, then no cut will be added. If relaxation is is a + PWMcCormickRelaxationData, then no cut will be added. + + Parameters + ---------- + keep_cut: bool + If keep_cut is True, then add_oa_point will also be called. Be careful if the relaxation object is relaxing + the nonconvex side of the constraint. Thus, the cut will be reconstructed when rebuild is called. If + keep_cut is False, then the cut will be discarded when rebuild is called. + check_violation: bool + If True, then a cut is only added if the cut generated would cut off the current point (current values + of the variables) by more than feasibility_tol. + feasibility_tol: float + Only used if check_violation is True + + Returns + ------- + new_con: pyomo.core.base.constraint._GeneralConstraintData + """ + rhs_vars = self.get_rhs_vars() + var_vals = tuple(v.value for v in rhs_vars) + + if var_vals in self._oa_points: + return None + + new_con = None + if self._has_a_convex_side(): + if check_violation: + needs_cut = False + try: + rhs_val = pe.value(self._get_expr_for_oa()) + except (OverflowError, ZeroDivisionError, ValueError): + rhs_val = None + if rhs_val is not None: + if self.has_convex_underestimator(): + viol = rhs_val - self.get_aux_var().value + else: + viol = self.get_aux_var().value - rhs_val + if viol > feasibility_tol: + needs_cut = True + else: + needs_cut = True + if needs_cut: + oa_cut = self._get_oa_cut() + new_con = self._add_oa_cut(pt_tuple=var_vals, oa_cut=oa_cut) + if keep_cut: + self._oa_points[var_vals] = oa_cut + + return new_con + + def clean_oa_points(self, ensure_oa_at_vertices=True): + if not self._has_a_convex_side(): + return + + rhs_vars = self.get_rhs_vars() + bnds_list: List[Tuple[float, float]] = list() + for v in rhs_vars: + bnds_list.append(_get_bnds_tuple(v)) + + for pt_tuple, oa_cut in list(self._oa_points.items()): + new_pt_list = list() + for (v_lb, v_ub), pt in zip(bnds_list, pt_tuple): + if pt < v_lb: + new_pt_list.append(v_lb) + elif pt > v_ub: + new_pt_list.append(v_ub) + else: + new_pt_list.append(pt) + new_pt_tuple = tuple(new_pt_list) + del self._oa_points[pt_tuple] + if new_pt_tuple in self._oa_points: + self._remove_oa_cut(oa_cut) + else: + self._oa_points[new_pt_tuple] = oa_cut + if ensure_oa_at_vertices: + lb_list = list() + ub_list = list() + for lb, ub in bnds_list: + if math.isfinite(lb) and math.isfinite(ub): + lb_list.append(lb) + ub_list.append(ub) + elif math.isfinite(lb): + lb_list.append(lb) + ub_list.append(max(lb + 1, 1)) + elif math.isfinite(ub): + lb_list.append(min(ub - 1, -1)) + ub_list.append(ub) + else: + lb_list.append(-1) + ub_list.append(1) + lb_tuple = tuple(lb_list) + ub_tuple = tuple(ub_list) + if lb_tuple not in self._oa_points: + if len(self._oa_points) <= 1: + self._add_oa_point(lb_tuple) + else: # move the smallest point to lb_tuple + min_pt = min(self._oa_points.keys()) + min_oa_cut = self._oa_points[min_pt] + del self._oa_points[min_pt] + self._oa_points[lb_tuple] = min_oa_cut + if ub_tuple not in self._oa_points: + if len(self._oa_points) <= 1: + self._add_oa_point(ub_tuple) + else: # move the largest point to ub_tuple + max_pt = max(self._oa_points.keys()) + max_oa_cut = self._oa_points[max_pt] + del self._oa_points[max_pt] + self._oa_points[ub_tuple] = max_oa_cut + + +@declare_custom_block(name='BasePWRelaxation') +class BasePWRelaxationData(BaseRelaxationData): + def __init__(self, component): + BaseRelaxationData.__init__(self, component) + + self._partitions = ComponentMap() # ComponentMap: var: list of float + self._saved_partitions = list() # list of CompnentMap + + def rebuild(self, build_nonlinear_constraint=False, ensure_oa_at_vertices=True): + """ + Remove any auto-created vars/constraints from the relaxation block and recreate it + """ + super(BasePWRelaxationData, self).rebuild(build_nonlinear_constraint=build_nonlinear_constraint, + ensure_oa_at_vertices=ensure_oa_at_vertices) + self.clean_partitions() + + def set_input(self, relaxation_side=RelaxationSide.BOTH, use_linear_relaxation=True, large_coef=1e5, + small_coef=1e-10, safety_tol=1e-10): + super(BasePWRelaxationData, self).set_input(relaxation_side=relaxation_side, + use_linear_relaxation=use_linear_relaxation, large_coef=large_coef, + small_coef=small_coef, safety_tol=safety_tol) + self._partitions = ComponentMap() + self._saved_partitions = list() + + def add_partition_point(self): + """ + Add a point to the current partitioning. This does not rebuild the relaxation. You must call rebuild() + to rebuild the relaxation. + """ + raise NotImplementedError('This method should be implemented in the derived class.') + + def _add_partition_point(self, var, value=None): + if value is None: + value = pe.value(var) + # if the point is outside the variable's bounds, then it will simply get removed when clean_partitions + # gets called. + self._partitions[var].append(value) + + def push_partitions(self): + """ + Save the current partitioning for later use through pop_partitions(). + """ + self._saved_partitions.append(pe.ComponentMap((k, list(v)) for k, v in self._partitions.items())) + + def clear_partitions(self): + """ + Delete any existing partitioning scheme. + """ + tmp = ComponentMap() + for var, pts in self._partitions.items(): + tmp[var] = [pe.value(var.lb), pe.value(var.ub)] + self._partitions = tmp + + def pop_partitions(self): + """ + Use the most recently saved partitioning. + """ + self._partitions = self._saved_partitions.pop(-1) + + def clean_partitions(self): + # discard any points in the partitioning that are not within the variable bounds + for var, pts in list(self._partitions.items()): + pts = list(set(pts)) + pts.sort() + self._partitions[var] = pts + + for var, pts in self._partitions.items(): + lb, ub = tuple(_get_bnds_list(var)) + + new_pts = list() + new_pts.append(lb) + for val in pts[1:-1]: + if lb < val < ub: + new_pts.append(val) + new_pts.append(ub) + self._partitions[var] = new_pts + + def get_active_partitions(self): + ans = ComponentMap() + for var, pts in self._partitions.items(): + val = pyo.value(var) + lower = None + upper = None + if not (pts[0] - 1e-6 <= val <= pts[-1] + 1e-6): + raise ValueError('The variable value must be within the variable bounds') + if val < pts[0]: + lower = pts[0] + upper = pts[1] + elif val > pts[-1]: + lower = pts[-2] + upper = pts[-1] + else: + for p1, p2 in zip(pts[0:-1], pts[1:]): + if p1 <= val <= p2: + lower = p1 + upper = p2 + break + assert lower is not None + assert upper is not None + ans[var] = lower, upper + return ans + + +class ComponentWeakRef(object): + """ + This object is used to reference components from a block that are not owned by that block. + """ + # ToDo: Example in the documentation + def __init__(self, comp): + self.compref = None + self.set_component(comp) + + def get_component(self): + if self.compref is None: + return None + return self.compref() + + def set_component(self, comp): + self.compref = None + if comp is not None: + self.compref = weakref.ref(comp) + + def __setstate__(self, state): + self.set_component(state['compref']) + + def __getstate__(self): + return {'compref': self.get_component()} diff --git a/pyomo/contrib/coramin/relaxations/segments.py b/pyomo/contrib/coramin/relaxations/segments.py new file mode 100644 index 00000000000..2cd71086540 --- /dev/null +++ b/pyomo/contrib/coramin/relaxations/segments.py @@ -0,0 +1,24 @@ +import pyomo.environ as pyo +import warnings +import logging + +logger = logging.getLogger(__name__) + + +def compute_k_segment_points(v, k): + """ + Return a list of points that generats k segments between v.lb and v.ub + + Parameters + ---------- + v: pyo.Var + k: int + + Returns + ------- + pts: list of float + """ + delta = (pyo.value(v.ub) - pyo.value(v.lb)) / k + pts = [pyo.value(v.lb) + i * delta for i in range(k)] + pts.append(pyo.value(v.ub)) + return pts diff --git a/pyomo/contrib/coramin/relaxations/split_expr.py b/pyomo/contrib/coramin/relaxations/split_expr.py new file mode 100644 index 00000000000..c879750aea9 --- /dev/null +++ b/pyomo/contrib/coramin/relaxations/split_expr.py @@ -0,0 +1,177 @@ +from pyomo.core.expr import numeric_expr +from pyomo.core.expr.visitor import identify_variables, ExpressionValueVisitor +from pyomo.core.expr.numvalue import ( + nonpyomo_leaf_types, + NumericValue, + is_potentially_variable, +) +from typing import MutableMapping, Tuple, Sequence, Union, List + + +def _flatten_expr_ProductExpression( + node: numeric_expr.ProductExpression, + values: Union[Tuple[NumericValue, ...], List[NumericValue]], +): + arg1, arg2 = values + arg1_type = type(arg1) + arg2_type = type(arg2) + if is_potentially_variable(arg1) and is_potentially_variable(arg2): + res = numeric_expr.ProductExpression(values) + elif is_potentially_variable(arg1): + if arg1_type is numeric_expr.SumExpression: + res = numeric_expr.SumExpression([arg2 * i for i in arg1.args]) + elif arg1_type is numeric_expr.LinearExpression: + res = numeric_expr.LinearExpression( + constant=arg2 * arg1.constant, + linear_coefs=[arg2 * i for i in arg1.linear_coefs], + linear_vars=list(arg1.linear_vars), + ) + else: + res = numeric_expr.ProductExpression(values) + elif is_potentially_variable(arg2): + if arg2_type is numeric_expr.SumExpression: + res = numeric_expr.SumExpression([arg1 * i for i in arg2.args]) + elif arg2_type is numeric_expr.LinearExpression: + res = numeric_expr.LinearExpression( + constant=arg1 * arg2.constant, + linear_coefs=[arg1 * i for i in arg2.linear_coefs], + linear_vars=list(arg2.linear_vars), + ) + else: + res = numeric_expr.ProductExpression(values) + else: + res = numeric_expr.ProductExpression(values) + return res + + +def _flatten_expr_SumExpression( + node: numeric_expr.SumExpression, + values: Union[Tuple[NumericValue, ...], List[NumericValue]], +): + all_args = list() + for arg in values: + if isinstance(arg, numeric_expr.SumExpression): + all_args.extend(arg.args) + elif isinstance(arg, numeric_expr.LinearExpression): + for c, v in zip(arg.linear_vars, arg.linear_coefs): + all_args.append(numeric_expr.MonomialTermExpression((c, v))) + all_args.append(arg.constant) + else: + all_args.append(arg) + return numeric_expr.SumExpression(all_args) + + +def _flatten_expr_NegationExpression( + node: numeric_expr.NegationExpression, + values: Union[Tuple[NumericValue, ...], List[NumericValue]], +): + assert len(values) == 1 + arg = values[0] + if isinstance(arg, numeric_expr.SumExpression): + res = numeric_expr.SumExpression([-i for i in arg.args]) + elif isinstance(arg, numeric_expr.LinearExpression): + new_args = [ + numeric_expr.MonomialTermExpression((-c, v)) + for c, v in zip(arg.linear_vars, arg.linear_coefs) + ] + new_args.append(-arg.constant) + res = numeric_expr.SumExpression(new_args) + else: + res = numeric_expr.NegationExpression((arg,)) + return res + + +def _flatten_expr_default( + node: numeric_expr.ExpressionBase, + values: Union[Tuple[NumericValue, ...], List[NumericValue]], +): + return node.create_node_with_local_data(tuple(values)) + + +_flatten_expr_map = dict() +_flatten_expr_map[numeric_expr.SumExpression] = _flatten_expr_SumExpression +_flatten_expr_map[numeric_expr.NegationExpression] = _flatten_expr_NegationExpression +_flatten_expr_map[numeric_expr.ProductExpression] = _flatten_expr_ProductExpression + + +class FlattenExprVisitor(ExpressionValueVisitor): + def visit(self, node, values): + node_type = type(node) + if node_type in _flatten_expr_map: + return _flatten_expr_map[node_type](node, values) + else: + return _flatten_expr_default(node, values) + + def visiting_potential_leaf(self, node): + node_type = type(node) + if node_type in nonpyomo_leaf_types: + return True, node + elif not node.is_expression_type(): + return True, node + elif node_type is numeric_expr.LinearExpression: + return True, node + else: + return False, None + + +def flatten_expr(expr): + visitor = FlattenExprVisitor() + return visitor.dfs_postorder_stack(expr) + + +class Grouper(object): + def __init__(self): + self._terms_by_num_var: MutableMapping[ + int, MutableMapping[Tuple[int, ...], NumericValue] + ] = dict() + + def add_term(self, expr): + vlist = list(identify_variables(expr=expr, include_fixed=False)) + vlist.sort(key=lambda x: id(x)) + v_ids = tuple(id(v) for v in vlist) + num_vars = len(vlist) + if num_vars not in self._terms_by_num_var: + self._terms_by_num_var[num_vars] = dict() + if v_ids not in self._terms_by_num_var[num_vars]: + self._terms_by_num_var[num_vars][v_ids] = expr + else: + self._terms_by_num_var[num_vars][v_ids] += expr + + def group(self) -> Sequence[NumericValue]: + num_var_list = list(self._terms_by_num_var.keys()) + num_var_list.sort(reverse=True) + for num_vars in num_var_list[1:]: + for last_num_vars in num_var_list: + if last_num_vars == num_vars: + break + for v_ids in list(self._terms_by_num_var[num_vars].keys()): + v_id_set = set(v_ids) + for last_v_ids in list( + self._terms_by_num_var[last_num_vars].keys() + ): + last_v_id_set = set(last_v_ids) + if len(v_id_set - last_v_id_set) == 0: + self._terms_by_num_var[last_num_vars][ + last_v_ids + ] += self._terms_by_num_var[num_vars][v_ids] + del self._terms_by_num_var[num_vars][v_ids] + break + + expr_list = list() + for num_vars in reversed(num_var_list): + for e in self._terms_by_num_var[num_vars].values(): + expr_list.append(e) + + return expr_list + + +def split_expr(expr): + expr = flatten_expr(expr) + if type(expr) is numeric_expr.SumExpression: + grouper = Grouper() + for arg in expr.args: + grouper.add_term(arg) + res = grouper.group() + else: + res = [expr] + return res diff --git a/pyomo/contrib/coramin/relaxations/tests/test_alphabb.py b/pyomo/contrib/coramin/relaxations/tests/test_alphabb.py new file mode 100644 index 00000000000..08b06c88428 --- /dev/null +++ b/pyomo/contrib/coramin/relaxations/tests/test_alphabb.py @@ -0,0 +1,59 @@ +import unittest +import itertools +import math +import pyomo.environ as pe +import coramin +from coramin.relaxations.alphabb import AlphaBBRelaxation + + +class TestAlphaBBRelaxation(unittest.TestCase): + @classmethod + def setUpClass(cls): + model = pe.ConcreteModel() + cls.model = model + model.x = pe.Var(bounds=(-2, 1)) + model.y = pe.Var(bounds=(-1, 1)) + model.w = pe.Var() + + model.f_x = pe.cos(model.x)*pe.sin(model.y) - model.x/(model.y**2 + 1) + + model.obj = pe.Objective(expr=model.w) + model.abb = AlphaBBRelaxation() + model.abb.build( + aux_var=model.w, f_x_expr=model.f_x, + relaxation_side=coramin.RelaxationSide.UNDER, + eigenvalue_bounder=coramin.EigenValueBounder.GershgorinWithSimplification + ) + + def test_nonlinear(self): + model = self.model.clone() + model.abb.use_linear_relaxation = False + model.abb.rebuild() + + model.w.value = 0.0 + + for x_v in [model.x.lb, model.x.ub]: + for y_v in [model.y.lb, model.y.ub]: + model.x.value = x_v + model.y.value = y_v + f_x_v = pe.value(model.f_x) + abb_v = pe.value(model.abb._nonlinear.body) + self.assertAlmostEqual(f_x_v, abb_v) + + solver = pe.SolverFactory('ipopt') + solver.solve(model) + self.assertLessEqual(model.w.value, pe.value(model.f_x)) + + def test_linear(self): + model = self.model.clone() + model.abb.use_linear_relaxation = True + + model.x.value = 0.0 + model.y.value = 0.0 + + for _ in range(5): + model.abb.add_oa_point() + model.abb.rebuild() + solver = pe.SolverFactory('appsi_gurobi') + solver.solve(model) + self.assertLessEqual(model.w.value, pe.value(model.f_x)) diff --git a/pyomo/contrib/coramin/relaxations/tests/test_auto_relax.py b/pyomo/contrib/coramin/relaxations/tests/test_auto_relax.py new file mode 100644 index 00000000000..e2f2a56fb41 --- /dev/null +++ b/pyomo/contrib/coramin/relaxations/tests/test_auto_relax.py @@ -0,0 +1,1112 @@ +import pyomo.environ as pe +import coramin +import unittest +from pyomo.core.expr.calculus.diff_with_pyomo import reverse_sd +from pyomo.core.expr.visitor import identify_variables, identify_components +import math +from pyomo.common.collections import ComponentSet +import numpy as np +from pyomo.core.base.param import _ParamData, ScalarParam +from pyomo.core.expr.sympy_tools import sympyify_expression +from pyomo.contrib import appsi +from coramin.utils import RelaxationSide, Effort, EigenValueBounder + + +class TestAutoRelax(unittest.TestCase): + def test_product1(self): + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(-1,1)) + m.y = pe.Var(bounds=(-1,1)) + m.z = pe.Var() + m.c = pe.Constraint(expr=m.z - m.x*m.y == 0) + + rel = coramin.relaxations.relax(m, use_alpha_bb=False) + + self.assertTrue(hasattr(rel, 'aux_cons')) + self.assertTrue(hasattr(rel, 'aux_vars')) + self.assertEqual(len(rel.aux_cons), 1) + self.assertEqual(len(rel.aux_vars), 1) + self.assertAlmostEqual(rel.aux_vars[1].lb, -1) + self.assertAlmostEqual(rel.aux_vars[1].ub, 1) + self.assertEqual(rel.aux_cons[1].lower, 0) + self.assertEqual(rel.aux_cons[1].upper, 0) + ders = reverse_sd(rel.aux_cons[1].body) + self.assertEqual(ders[rel.z], 1) + self.assertEqual(ders[rel.aux_vars[1]], -1) + self.assertEqual(len(list(identify_variables(rel.aux_cons[1].body))), 2) + + self.assertTrue(hasattr(rel, 'relaxations')) + self.assertTrue(hasattr(rel.relaxations, 'rel0')) + self.assertTrue(isinstance(rel.relaxations.rel0, coramin.relaxations.PWMcCormickRelaxation)) + self.assertIn(rel.x, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) + self.assertIn(rel.y, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) + self.assertEqual(id(rel.aux_vars[1]), id(rel.relaxations.rel0.get_aux_var())) + + def test_product2(self): + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(-1,1)) + m.y = pe.Var(bounds=(-1,1)) + m.z = pe.Var() + m.v = pe.Var() + m.c1 = pe.Constraint(expr=m.z - m.x*m.y == 0) + m.c2 = pe.Constraint(expr=m.v - 3*m.x*m.y == 0) + + rel = coramin.relaxations.relax(m, use_alpha_bb=False) + + self.assertTrue(hasattr(rel, 'aux_cons')) + self.assertTrue(hasattr(rel, 'aux_vars')) + self.assertEqual(len(rel.aux_cons), 2) + self.assertEqual(len(rel.aux_vars), 1) + self.assertAlmostEqual(rel.aux_vars[1].lb, -1) + self.assertAlmostEqual(rel.aux_vars[1].ub, 1) + + self.assertEqual(rel.aux_cons[1].lower, 0) + self.assertEqual(rel.aux_cons[1].upper, 0) + ders = reverse_sd(rel.aux_cons[1].body) + self.assertEqual(ders[rel.z], 1) + self.assertEqual(ders[rel.aux_vars[1]], -1) + self.assertEqual(len(list(identify_variables(rel.aux_cons[1].body))), 2) + + self.assertEqual(rel.aux_cons[2].lower, 0) + self.assertEqual(rel.aux_cons[2].upper, 0) + ders = reverse_sd(rel.aux_cons[2].body) + self.assertEqual(ders[rel.v], 1) + self.assertEqual(ders[rel.aux_vars[1]], -3) + self.assertEqual(len(list(identify_variables(rel.aux_cons[2].body))), 2) + + self.assertTrue(hasattr(rel, 'relaxations')) + self.assertTrue(hasattr(rel.relaxations, 'rel0')) + self.assertTrue(isinstance(rel.relaxations.rel0, coramin.relaxations.PWMcCormickRelaxation)) + self.assertIn(rel.x, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) + self.assertIn(rel.y, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) + self.assertEqual(id(rel.aux_vars[1]), id(rel.relaxations.rel0.get_aux_var())) + + def test_product3(self): + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(-1,1)) + m.y = pe.Var(bounds=(-1,1)) + m.z = pe.Var() + m.c = pe.Constraint(expr=m.z - m.x*m.y*3 == 0) + + rel = coramin.relaxations.relax(m, use_alpha_bb=False) + + self.assertTrue(hasattr(rel, 'aux_cons')) + self.assertTrue(hasattr(rel, 'aux_vars')) + self.assertEqual(len(rel.aux_cons), 1) + self.assertEqual(len(rel.aux_vars), 1) + self.assertAlmostEqual(rel.aux_vars[1].lb, -1) + self.assertAlmostEqual(rel.aux_vars[1].ub, 1) + self.assertEqual(rel.aux_cons[1].lower, 0) + self.assertEqual(rel.aux_cons[1].upper, 0) + ders = reverse_sd(rel.aux_cons[1].body) + self.assertEqual(ders[rel.z], 1) + self.assertEqual(ders[rel.aux_vars[1]], -3) + self.assertEqual(len(list(identify_variables(rel.aux_cons[1].body))), 2) + + self.assertTrue(hasattr(rel, 'relaxations')) + self.assertTrue(hasattr(rel.relaxations, 'rel0')) + self.assertTrue(isinstance(rel.relaxations.rel0, coramin.relaxations.PWMcCormickRelaxation)) + self.assertIn(rel.x, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) + self.assertIn(rel.y, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) + self.assertEqual(id(rel.aux_vars[1]), id(rel.relaxations.rel0.get_aux_var())) + relaxations = list(coramin.relaxations.relaxation_data_objects(rel)) + self.assertEqual(len(relaxations), 1) + + def test_product4(self): + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(-1,1)) + m.z = pe.Var() + m.c = pe.Constraint(expr=m.z - m.x*m.x == 0) + + rel = coramin.relaxations.relax(m, use_alpha_bb=False) + + self.assertTrue(hasattr(rel, 'aux_cons')) + self.assertTrue(hasattr(rel, 'aux_vars')) + self.assertEqual(len(rel.aux_cons), 1) + self.assertEqual(len(rel.aux_vars), 1) + self.assertAlmostEqual(rel.aux_vars[1].lb, 0) + self.assertAlmostEqual(rel.aux_vars[1].ub, 1) + self.assertEqual(rel.aux_cons[1].lower, 0) + self.assertEqual(rel.aux_cons[1].upper, 0) + ders = reverse_sd(rel.aux_cons[1].body) + self.assertEqual(ders[rel.z], 1) + self.assertEqual(ders[rel.aux_vars[1]], -1) + self.assertEqual(len(list(identify_variables(rel.aux_cons[1].body))), 2) + + self.assertTrue(hasattr(rel, 'relaxations')) + self.assertTrue(hasattr(rel.relaxations, 'rel0')) + self.assertTrue(isinstance(rel.relaxations.rel0, coramin.relaxations.PWXSquaredRelaxationData)) + self.assertIn(rel.x, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) + self.assertEqual(len(rel.relaxations.rel0.get_rhs_vars()), 1) + self.assertEqual(id(rel.aux_vars[1]), id(rel.relaxations.rel0.get_aux_var())) + relaxations = list(coramin.relaxations.relaxation_data_objects(rel)) + self.assertEqual(len(relaxations), 1) + + def test_quadratic(self): + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(-1,1)) + m.y = pe.Var() + m.z = pe.Var() + m.w = pe.Var() + m.c = pe.Constraint(expr=m.x**2 + m.y + m.z == 0) + m.c2 = pe.Constraint(expr=m.w - 3*m.x**2 == 0) + + rel = coramin.relaxations.relax(m, use_alpha_bb=False) + + self.assertTrue(hasattr(rel, 'aux_cons')) + self.assertTrue(hasattr(rel, 'aux_vars')) + self.assertEqual(len(rel.aux_cons), 2) + self.assertEqual(len(rel.aux_vars), 1) + self.assertAlmostEqual(rel.aux_vars[1].lb, 0) + self.assertAlmostEqual(rel.aux_vars[1].ub, 1) + + self.assertEqual(rel.aux_cons[1].lower, 0) + self.assertEqual(rel.aux_cons[1].upper, 0) + ders = reverse_sd(rel.aux_cons[1].body) + self.assertEqual(ders[rel.z], 1) + self.assertEqual(ders[rel.aux_vars[1]], 1) + self.assertEqual(ders[rel.y], 1) + self.assertEqual(len(list(identify_variables(rel.aux_cons[1].body))), 3) + + self.assertEqual(rel.aux_cons[2].lower, 0) + self.assertEqual(rel.aux_cons[2].upper, 0) + ders = reverse_sd(rel.aux_cons[2].body) + self.assertEqual(ders[rel.w], 1) + self.assertEqual(ders[rel.aux_vars[1]], -3) + self.assertEqual(len(list(identify_variables(rel.aux_cons[2].body))), 2) + + self.assertTrue(hasattr(rel, 'relaxations')) + self.assertTrue(hasattr(rel.relaxations, 'rel0')) + self.assertTrue(isinstance(rel.relaxations.rel0, coramin.relaxations.PWXSquaredRelaxation)) + self.assertIn(rel.x, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) + self.assertEqual(id(rel.aux_vars[1]), id(rel.relaxations.rel0.get_aux_var())) + self.assertFalse(hasattr(rel.relaxations, 'rel1')) + + def test_cubic_convex(self): + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(1,2)) + m.y = pe.Var() + m.z = pe.Var() + m.w = pe.Var() + m.c = pe.Constraint(expr=m.x**3 + m.y + m.z == 0) + m.c2 = pe.Constraint(expr=m.w - 3*m.x**3 == 0) + + rel = coramin.relaxations.relax(m, use_alpha_bb=False) + + self.assertTrue(hasattr(rel, 'aux_cons')) + self.assertTrue(hasattr(rel, 'aux_vars')) + self.assertEqual(len(rel.aux_cons), 2) + self.assertEqual(len(rel.aux_vars), 1) + self.assertAlmostEqual(rel.aux_vars[1].lb, 1) + self.assertAlmostEqual(rel.aux_vars[1].ub, 8) + + self.assertEqual(rel.aux_cons[1].lower, 0) + self.assertEqual(rel.aux_cons[1].upper, 0) + ders = reverse_sd(rel.aux_cons[1].body) + self.assertEqual(ders[rel.z], 1) + self.assertEqual(ders[rel.aux_vars[1]], 1) + self.assertEqual(ders[rel.y], 1) + self.assertEqual(len(list(identify_variables(rel.aux_cons[1].body))), 3) + + self.assertEqual(rel.aux_cons[2].lower, 0) + self.assertEqual(rel.aux_cons[2].upper, 0) + ders = reverse_sd(rel.aux_cons[2].body) + self.assertEqual(ders[rel.w], 1) + self.assertEqual(ders[rel.aux_vars[1]], -3) + self.assertEqual(len(list(identify_variables(rel.aux_cons[2].body))), 2) + + self.assertTrue(hasattr(rel, 'relaxations')) + self.assertTrue(hasattr(rel.relaxations, 'rel0')) + self.assertTrue(isinstance(rel.relaxations.rel0, coramin.relaxations.PWUnivariateRelaxation)) + self.assertIn(rel.x, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) + self.assertEqual(id(rel.aux_vars[1]), id(rel.relaxations.rel0.get_aux_var())) + self.assertTrue(rel.relaxations.rel0.is_rhs_convex()) + self.assertFalse(rel.relaxations.rel0.is_rhs_concave()) + self.assertFalse(hasattr(rel.relaxations, 'rel1')) + + def test_cubic_concave(self): + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(-2,-1)) + m.y = pe.Var() + m.z = pe.Var() + m.w = pe.Var() + m.c = pe.Constraint(expr=m.x**3 + m.y + m.z == 0) + m.c2 = pe.Constraint(expr=m.w - 3*m.x**3 == 0) + + rel = coramin.relaxations.relax(m, use_alpha_bb=False) + + self.assertTrue(hasattr(rel, 'aux_cons')) + self.assertTrue(hasattr(rel, 'aux_vars')) + self.assertEqual(len(rel.aux_cons), 2) + self.assertEqual(len(rel.aux_vars), 1) + self.assertAlmostEqual(rel.aux_vars[1].lb, -8) + self.assertAlmostEqual(rel.aux_vars[1].ub, -1) + + self.assertEqual(rel.aux_cons[1].lower, 0) + self.assertEqual(rel.aux_cons[1].upper, 0) + ders = reverse_sd(rel.aux_cons[1].body) + self.assertEqual(ders[rel.z], 1) + self.assertEqual(ders[rel.aux_vars[1]], 1) + self.assertEqual(ders[rel.y], 1) + self.assertEqual(len(list(identify_variables(rel.aux_cons[1].body))), 3) + + self.assertEqual(rel.aux_cons[2].lower, 0) + self.assertEqual(rel.aux_cons[2].upper, 0) + ders = reverse_sd(rel.aux_cons[2].body) + self.assertEqual(ders[rel.w], 1) + self.assertEqual(ders[rel.aux_vars[1]], -3) + self.assertEqual(len(list(identify_variables(rel.aux_cons[2].body))), 2) + + self.assertTrue(hasattr(rel, 'relaxations')) + self.assertTrue(hasattr(rel.relaxations, 'rel0')) + self.assertTrue(isinstance(rel.relaxations.rel0, coramin.relaxations.PWUnivariateRelaxation)) + self.assertIn(rel.x, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) + self.assertEqual(id(rel.aux_vars[1]), id(rel.relaxations.rel0.get_aux_var())) + self.assertFalse(rel.relaxations.rel0.is_rhs_convex()) + self.assertTrue(rel.relaxations.rel0.is_rhs_concave()) + self.assertFalse(hasattr(rel.relaxations, 'rel1')) + + def test_cubic(self): + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(-1,1)) + m.y = pe.Var() + m.z = pe.Var() + m.w = pe.Var() + m.c = pe.Constraint(expr=m.x**3 + m.y + m.z == 0) + m.c2 = pe.Constraint(expr=m.w - 3*m.x**3 == 0) + + rel = coramin.relaxations.relax(m, use_alpha_bb=False) + + # this problem should turn into + # + # aux2 + y + z = 0 => aux_con[1] + # w - 3*aux2 = 0 => aux_con[2] + # aux1 = x**2 => rel0 + # aux2 = x*aux1 => rel1 + + self.assertTrue(hasattr(rel, 'aux_cons')) + self.assertTrue(hasattr(rel, 'aux_vars')) + self.assertEqual(len(rel.aux_cons), 2) + self.assertEqual(len(rel.aux_vars), 2) + + self.assertAlmostEqual(rel.aux_vars[1].lb, 0) + self.assertAlmostEqual(rel.aux_vars[1].ub, 1) + + self.assertAlmostEqual(rel.aux_vars[2].lb, -1) + self.assertAlmostEqual(rel.aux_vars[2].ub, 1) + + self.assertEqual(rel.aux_cons[1].lower, 0) + self.assertEqual(rel.aux_cons[1].upper, 0) + ders = reverse_sd(rel.aux_cons[1].body) + self.assertEqual(ders[rel.z], 1) + self.assertEqual(ders[rel.aux_vars[2]], 1) + self.assertEqual(ders[rel.y], 1) + self.assertEqual(len(list(identify_variables(rel.aux_cons[1].body))), 3) + + self.assertEqual(rel.aux_cons[2].lower, 0) + self.assertEqual(rel.aux_cons[2].upper, 0) + ders = reverse_sd(rel.aux_cons[2].body) + self.assertEqual(ders[rel.w], 1) + self.assertEqual(ders[rel.aux_vars[2]], -3) + self.assertEqual(len(list(identify_variables(rel.aux_cons[2].body))), 2) + + self.assertTrue(hasattr(rel, 'relaxations')) + self.assertTrue(hasattr(rel.relaxations, 'rel0')) + self.assertTrue(isinstance(rel.relaxations.rel0, coramin.relaxations.PWXSquaredRelaxation)) + self.assertIn(rel.x, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) + self.assertEqual(id(rel.aux_vars[1]), id(rel.relaxations.rel0.get_aux_var())) + + self.assertTrue(hasattr(rel.relaxations, 'rel1')) + self.assertTrue(isinstance(rel.relaxations.rel1, coramin.relaxations.PWMcCormickRelaxation)) + self.assertIn(rel.x, ComponentSet(rel.relaxations.rel1.get_rhs_vars())) + self.assertIn(rel.aux_vars[1], ComponentSet(rel.relaxations.rel1.get_rhs_vars())) + self.assertEqual(id(rel.aux_vars[2]), id(rel.relaxations.rel1.get_aux_var())) + + def test_pow_fractional1(self): + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(-1,1)) + m.y = pe.Var() + m.z = pe.Var() + m.w = pe.Var() + m.p = pe.Param(initialize=0.5) + m.c = pe.Constraint(expr=m.x**m.p + m.y + m.z == 0) + m.c2 = pe.Constraint(expr=m.w - 3*m.x**m.p == 0) + + rel = coramin.relaxations.relax(m, use_alpha_bb=False) + + self.assertTrue(hasattr(rel, 'aux_cons')) + self.assertTrue(hasattr(rel, 'aux_vars')) + self.assertEqual(len(rel.aux_cons), 2) + self.assertEqual(len(rel.aux_vars), 1) + self.assertAlmostEqual(rel.aux_vars[1].lb, 0) + self.assertAlmostEqual(rel.aux_vars[1].ub, 1) + + self.assertEqual(rel.aux_cons[1].lower, 0) + self.assertEqual(rel.aux_cons[1].upper, 0) + ders = reverse_sd(rel.aux_cons[1].body) + self.assertEqual(ders[rel.z], 1) + self.assertEqual(ders[rel.aux_vars[1]], 1) + self.assertEqual(ders[rel.y], 1) + self.assertEqual(len(list(identify_variables(rel.aux_cons[1].body))), 3) + + self.assertEqual(rel.aux_cons[2].lower, 0) + self.assertEqual(rel.aux_cons[2].upper, 0) + ders = reverse_sd(rel.aux_cons[2].body) + self.assertEqual(ders[rel.w], 1) + self.assertEqual(ders[rel.aux_vars[1]], -3) + self.assertEqual(len(list(identify_variables(rel.aux_cons[2].body))), 2) + + self.assertTrue(hasattr(rel, 'relaxations')) + self.assertTrue(hasattr(rel.relaxations, 'rel0')) + self.assertTrue(isinstance(rel.relaxations.rel0, coramin.relaxations.PWUnivariateRelaxation)) + self.assertIn(rel.x, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) + self.assertEqual(id(rel.aux_vars[1]), id(rel.relaxations.rel0.get_aux_var())) + self.assertFalse(rel.relaxations.rel0.is_rhs_convex()) + self.assertTrue(rel.relaxations.rel0.is_rhs_concave()) + self.assertFalse(hasattr(rel.relaxations, 'rel1')) + + def test_pow_fractional2(self): + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(-1,1)) + m.y = pe.Var() + m.z = pe.Var() + m.w = pe.Var() + m.p = pe.Param(initialize=1.5) + m.c = pe.Constraint(expr=m.x**m.p + m.y + m.z == 0) + m.c2 = pe.Constraint(expr=m.w - 3*m.x**m.p == 0) + + rel = coramin.relaxations.relax(m, use_alpha_bb=False) + + self.assertTrue(hasattr(rel, 'aux_cons')) + self.assertTrue(hasattr(rel, 'aux_vars')) + self.assertEqual(len(rel.aux_cons), 2) + self.assertEqual(len(rel.aux_vars), 1) + self.assertAlmostEqual(rel.aux_vars[1].lb, 0) + self.assertAlmostEqual(rel.aux_vars[1].ub, 1) + + self.assertEqual(rel.aux_cons[1].lower, 0) + self.assertEqual(rel.aux_cons[1].upper, 0) + ders = reverse_sd(rel.aux_cons[1].body) + self.assertEqual(ders[rel.z], 1) + self.assertEqual(ders[rel.aux_vars[1]], 1) + self.assertEqual(ders[rel.y], 1) + self.assertEqual(len(list(identify_variables(rel.aux_cons[1].body))), 3) + + self.assertEqual(rel.aux_cons[2].lower, 0) + self.assertEqual(rel.aux_cons[2].upper, 0) + ders = reverse_sd(rel.aux_cons[2].body) + self.assertEqual(ders[rel.w], 1) + self.assertEqual(ders[rel.aux_vars[1]], -3) + self.assertEqual(len(list(identify_variables(rel.aux_cons[2].body))), 2) + + self.assertTrue(hasattr(rel, 'relaxations')) + self.assertTrue(hasattr(rel.relaxations, 'rel0')) + self.assertTrue(isinstance(rel.relaxations.rel0, coramin.relaxations.PWUnivariateRelaxation)) + self.assertIn(rel.x, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) + self.assertEqual(id(rel.aux_vars[1]), id(rel.relaxations.rel0.get_aux_var())) + self.assertTrue(rel.relaxations.rel0.is_rhs_convex()) + self.assertFalse(rel.relaxations.rel0.is_rhs_concave()) + self.assertFalse(hasattr(rel.relaxations, 'rel1')) + + def test_pow_neg_even1(self): + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(1,2)) + m.y = pe.Var() + m.z = pe.Var() + m.w = pe.Var() + m.p = pe.Param(initialize=-2) + m.c = pe.Constraint(expr=m.x**m.p + m.y + m.z == 0) + m.c2 = pe.Constraint(expr=m.w - 3*m.x**m.p == 0) + + rel = coramin.relaxations.relax(m, use_alpha_bb=False) + + self.assertTrue(hasattr(rel, 'aux_cons')) + self.assertTrue(hasattr(rel, 'aux_vars')) + self.assertEqual(len(rel.aux_cons), 2) + self.assertEqual(len(rel.aux_vars), 1) + self.assertAlmostEqual(rel.aux_vars[1].lb, 0.25) + self.assertAlmostEqual(rel.aux_vars[1].ub, 1) + + self.assertEqual(rel.aux_cons[1].lower, 0) + self.assertEqual(rel.aux_cons[1].upper, 0) + ders = reverse_sd(rel.aux_cons[1].body) + self.assertEqual(ders[rel.z], 1) + self.assertEqual(ders[rel.aux_vars[1]], 1) + self.assertEqual(ders[rel.y], 1) + self.assertEqual(len(list(identify_variables(rel.aux_cons[1].body))), 3) + + self.assertEqual(rel.aux_cons[2].lower, 0) + self.assertEqual(rel.aux_cons[2].upper, 0) + ders = reverse_sd(rel.aux_cons[2].body) + self.assertEqual(ders[rel.w], 1) + self.assertEqual(ders[rel.aux_vars[1]], -3) + self.assertEqual(len(list(identify_variables(rel.aux_cons[2].body))), 2) + + self.assertTrue(hasattr(rel, 'relaxations')) + self.assertTrue(hasattr(rel.relaxations, 'rel0')) + self.assertTrue(isinstance(rel.relaxations.rel0, coramin.relaxations.PWUnivariateRelaxation)) + self.assertIn(rel.x, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) + self.assertEqual(id(rel.aux_vars[1]), id(rel.relaxations.rel0.get_aux_var())) + self.assertTrue(rel.relaxations.rel0.is_rhs_convex()) + self.assertFalse(rel.relaxations.rel0.is_rhs_concave()) + self.assertFalse(hasattr(rel.relaxations, 'rel1')) + + def test_pow_neg_even2(self): + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(-2,-1)) + m.y = pe.Var() + m.z = pe.Var() + m.w = pe.Var() + m.p = pe.Param(initialize=-2) + m.c = pe.Constraint(expr=m.x**m.p + m.y + m.z == 0) + m.c2 = pe.Constraint(expr=m.w - 3*m.x**m.p == 0) + + rel = coramin.relaxations.relax(m, use_alpha_bb=False) + + self.assertTrue(hasattr(rel, 'aux_cons')) + self.assertTrue(hasattr(rel, 'aux_vars')) + self.assertEqual(len(rel.aux_cons), 2) + self.assertEqual(len(rel.aux_vars), 1) + self.assertAlmostEqual(rel.aux_vars[1].lb, 0.25) + self.assertAlmostEqual(rel.aux_vars[1].ub, 1) + + self.assertEqual(rel.aux_cons[1].lower, 0) + self.assertEqual(rel.aux_cons[1].upper, 0) + ders = reverse_sd(rel.aux_cons[1].body) + self.assertEqual(ders[rel.z], 1) + self.assertEqual(ders[rel.aux_vars[1]], 1) + self.assertEqual(ders[rel.y], 1) + self.assertEqual(len(list(identify_variables(rel.aux_cons[1].body))), 3) + + self.assertEqual(rel.aux_cons[2].lower, 0) + self.assertEqual(rel.aux_cons[2].upper, 0) + ders = reverse_sd(rel.aux_cons[2].body) + self.assertEqual(ders[rel.w], 1) + self.assertEqual(ders[rel.aux_vars[1]], -3) + self.assertEqual(len(list(identify_variables(rel.aux_cons[2].body))), 2) + + self.assertTrue(hasattr(rel, 'relaxations')) + self.assertTrue(hasattr(rel.relaxations, 'rel0')) + self.assertTrue(isinstance(rel.relaxations.rel0, coramin.relaxations.PWUnivariateRelaxation)) + self.assertIn(rel.x, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) + self.assertEqual(id(rel.aux_vars[1]), id(rel.relaxations.rel0.get_aux_var())) + self.assertTrue(rel.relaxations.rel0.is_rhs_convex()) + self.assertFalse(rel.relaxations.rel0.is_rhs_concave()) + self.assertFalse(hasattr(rel.relaxations, 'rel1')) + + def test_pow_neg_odd1(self): + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(1,2)) + m.y = pe.Var() + m.z = pe.Var() + m.w = pe.Var() + m.p = pe.Param(initialize=-3) + m.c = pe.Constraint(expr=m.x**m.p + m.y + m.z == 0) + m.c2 = pe.Constraint(expr=m.w - 3*m.x**m.p == 0) + + rel = coramin.relaxations.relax(m, use_alpha_bb=False) + + self.assertTrue(hasattr(rel, 'aux_cons')) + self.assertTrue(hasattr(rel, 'aux_vars')) + self.assertEqual(len(rel.aux_cons), 2) + self.assertEqual(len(rel.aux_vars), 1) + self.assertAlmostEqual(rel.aux_vars[1].lb, 0.125) + self.assertAlmostEqual(rel.aux_vars[1].ub, 1) + + self.assertEqual(rel.aux_cons[1].lower, 0) + self.assertEqual(rel.aux_cons[1].upper, 0) + ders = reverse_sd(rel.aux_cons[1].body) + self.assertEqual(ders[rel.z], 1) + self.assertEqual(ders[rel.aux_vars[1]], 1) + self.assertEqual(ders[rel.y], 1) + self.assertEqual(len(list(identify_variables(rel.aux_cons[1].body))), 3) + + self.assertEqual(rel.aux_cons[2].lower, 0) + self.assertEqual(rel.aux_cons[2].upper, 0) + ders = reverse_sd(rel.aux_cons[2].body) + self.assertEqual(ders[rel.w], 1) + self.assertEqual(ders[rel.aux_vars[1]], -3) + self.assertEqual(len(list(identify_variables(rel.aux_cons[2].body))), 2) + + self.assertTrue(hasattr(rel, 'relaxations')) + self.assertTrue(hasattr(rel.relaxations, 'rel0')) + self.assertTrue(isinstance(rel.relaxations.rel0, coramin.relaxations.PWUnivariateRelaxation)) + self.assertIn(rel.x, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) + self.assertEqual(id(rel.aux_vars[1]), id(rel.relaxations.rel0.get_aux_var())) + self.assertTrue(rel.relaxations.rel0.is_rhs_convex()) + self.assertFalse(rel.relaxations.rel0.is_rhs_concave()) + self.assertFalse(hasattr(rel.relaxations, 'rel1')) + + def test_pow_neg_odd2(self): + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(-2,-1)) + m.y = pe.Var() + m.z = pe.Var() + m.w = pe.Var() + m.p = pe.Param(initialize=-3) + m.c = pe.Constraint(expr=m.x**m.p + m.y + m.z == 0) + m.c2 = pe.Constraint(expr=m.w - 3*m.x**m.p == 0) + + rel = coramin.relaxations.relax(m, use_alpha_bb=False) + + self.assertTrue(hasattr(rel, 'aux_cons')) + self.assertTrue(hasattr(rel, 'aux_vars')) + self.assertEqual(len(rel.aux_cons), 2) + self.assertEqual(len(rel.aux_vars), 1) + self.assertAlmostEqual(rel.aux_vars[1].lb, -1) + self.assertAlmostEqual(rel.aux_vars[1].ub, -0.125) + + self.assertEqual(rel.aux_cons[1].lower, 0) + self.assertEqual(rel.aux_cons[1].upper, 0) + ders = reverse_sd(rel.aux_cons[1].body) + self.assertEqual(ders[rel.z], 1) + self.assertEqual(ders[rel.aux_vars[1]], 1) + self.assertEqual(ders[rel.y], 1) + self.assertEqual(len(list(identify_variables(rel.aux_cons[1].body))), 3) + + self.assertEqual(rel.aux_cons[2].lower, 0) + self.assertEqual(rel.aux_cons[2].upper, 0) + ders = reverse_sd(rel.aux_cons[2].body) + self.assertEqual(ders[rel.w], 1) + self.assertEqual(ders[rel.aux_vars[1]], -3) + self.assertEqual(len(list(identify_variables(rel.aux_cons[2].body))), 2) + + self.assertTrue(hasattr(rel, 'relaxations')) + self.assertTrue(hasattr(rel.relaxations, 'rel0')) + self.assertTrue(isinstance(rel.relaxations.rel0, coramin.relaxations.PWUnivariateRelaxation)) + self.assertIn(rel.x, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) + self.assertEqual(id(rel.aux_vars[1]), id(rel.relaxations.rel0.get_aux_var())) + self.assertFalse(rel.relaxations.rel0.is_rhs_convex()) + self.assertTrue(rel.relaxations.rel0.is_rhs_concave()) + self.assertFalse(hasattr(rel.relaxations, 'rel1')) + + def test_pow_neg(self): + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(-1,1)) + m.y = pe.Var() + m.z = pe.Var() + m.w = pe.Var() + m.p = pe.Param(initialize=-2) + m.c = pe.Constraint(expr=m.x**m.p + m.y + m.z == 0) + m.c2 = pe.Constraint(expr=m.w - 3*m.x**m.p == 0) + + rel = coramin.relaxations.relax(m, use_alpha_bb=False) + + # This model should be relaxed to + # + # aux2 + y + z = 0 + # w - 3 * aux2 = 0 + # aux1 = x**2 + # aux1*aux2 = aux3 + # aux3 = 1 + # + + self.assertTrue(hasattr(rel, 'aux_cons')) + self.assertTrue(hasattr(rel, 'aux_vars')) + self.assertEqual(len(rel.aux_cons), 2) + self.assertEqual(len(rel.aux_vars), 3) + + self.assertAlmostEqual(rel.aux_vars[1].lb, 0) + self.assertAlmostEqual(rel.aux_vars[1].ub, 1) + + self.assertTrue(rel.aux_vars[3].is_fixed()) + self.assertEqual(rel.aux_vars[3].value, 1) + + self.assertEqual(rel.aux_cons[1].lower, 0) + self.assertEqual(rel.aux_cons[1].upper, 0) + ders = reverse_sd(rel.aux_cons[1].body) + self.assertEqual(ders[rel.z], 1) + self.assertEqual(ders[rel.aux_vars[2]], 1) + self.assertEqual(ders[rel.y], 1) + self.assertEqual(len(list(identify_variables(rel.aux_cons[1].body))), 3) + + self.assertEqual(rel.aux_cons[2].lower, 0) + self.assertEqual(rel.aux_cons[2].upper, 0) + ders = reverse_sd(rel.aux_cons[2].body) + self.assertEqual(ders[rel.w], 1) + self.assertEqual(ders[rel.aux_vars[2]], -3) + self.assertEqual(len(list(identify_variables(rel.aux_cons[2].body))), 2) + + self.assertTrue(hasattr(rel, 'relaxations')) + self.assertTrue(hasattr(rel.relaxations, 'rel0')) + self.assertTrue(isinstance(rel.relaxations.rel0, coramin.relaxations.PWXSquaredRelaxation)) + self.assertIn(rel.x, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) + self.assertEqual(id(rel.aux_vars[1]), id(rel.relaxations.rel0.get_aux_var())) + self.assertTrue(rel.relaxations.rel0.is_rhs_convex()) + self.assertFalse(rel.relaxations.rel0.is_rhs_concave()) + + self.assertTrue(hasattr(rel.relaxations, 'rel1')) + self.assertTrue(isinstance(rel.relaxations.rel1, coramin.relaxations.PWMcCormickRelaxation)) + self.assertIn(rel.aux_vars[1], ComponentSet(rel.relaxations.rel1.get_rhs_vars())) + self.assertIn(rel.aux_vars[2], ComponentSet(rel.relaxations.rel1.get_rhs_vars())) + self.assertEqual(id(rel.aux_vars[3]), id(rel.relaxations.rel1.get_aux_var())) + + self.assertFalse(hasattr(rel.relaxations, 'rel2')) + + def test_sqrt(self): + m = pe.ConcreteModel() + m.x = pe.Var() + m.z = pe.Var() + m.c = pe.Constraint(expr=m.z + pe.sqrt(2*pe.log(m.x)) <= 1) + coramin.relaxations.relax(m, in_place=True, use_fbbt=False, use_alpha_bb=False) + rels = list(coramin.relaxations.relaxation_data_objects(m, descend_into=True, active=True, sort=True)) + self.assertEqual(len(rels), 2) + rel0 = m.relaxations.rel0 # log + rel1 = m.relaxations.rel1 # sqrt + self.assertEqual(sympyify_expression(rel0.get_rhs_expr() - pe.log(m.x))[1], 0) + self.assertEqual(sympyify_expression(rel1.get_rhs_expr() - m.aux_vars[3]**0.5)[1], 0) + self.assertEqual(sympyify_expression(m.aux_cons[1].body - m.aux_vars[3] + 2 * m.aux_vars[1])[1], 0) + self.assertEqual(sympyify_expression(m.aux_cons[2].body - m.z - m.aux_vars[2])[1], 0) + self.assertEqual(m.aux_cons[1].lower, 0) + self.assertEqual(m.aux_cons[2].lower, None) + self.assertEqual(m.aux_cons[2].upper, 1) + self.assertIs(rel0.get_aux_var(), m.aux_vars[1]) + self.assertIs(rel1.get_aux_var(), m.aux_vars[2]) + self.assertEqual(rel0.relaxation_side, coramin.utils.RelaxationSide.UNDER) + self.assertEqual(rel1.relaxation_side, coramin.utils.RelaxationSide.UNDER) + + def test_exp(self): + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(-1,1)) + m.y = pe.Var(bounds=(-1,1)) + m.z = pe.Var() + m.w = pe.Var() + m.c = pe.Constraint(expr=pe.exp(m.x*m.y) + m.z == 0) + m.c2 = pe.Constraint(expr=m.w - 3*pe.exp(m.x*m.y) == 0) + + rel = coramin.relaxations.relax(m, use_alpha_bb=False) + + self.assertTrue(hasattr(rel, 'aux_cons')) + self.assertTrue(hasattr(rel, 'aux_vars')) + self.assertEqual(len(rel.aux_cons), 2) + self.assertEqual(len(rel.aux_vars), 2) + + self.assertAlmostEqual(rel.aux_vars[1].lb, -1) + self.assertAlmostEqual(rel.aux_vars[1].ub, 1) + + self.assertAlmostEqual(rel.aux_vars[2].lb, math.exp(-1)) + self.assertAlmostEqual(rel.aux_vars[2].ub, math.exp(1)) + + self.assertEqual(rel.aux_cons[1].lower, 0) + self.assertEqual(rel.aux_cons[1].upper, 0) + ders = reverse_sd(rel.aux_cons[1].body) + self.assertEqual(ders[rel.z], 1) + self.assertEqual(ders[rel.aux_vars[2]], 1) + self.assertEqual(len(list(identify_variables(rel.aux_cons[1].body))), 2) + + self.assertEqual(rel.aux_cons[2].lower, 0) + self.assertEqual(rel.aux_cons[2].upper, 0) + ders = reverse_sd(rel.aux_cons[2].body) + self.assertEqual(ders[rel.w], 1) + self.assertEqual(ders[rel.aux_vars[2]], -3) + self.assertEqual(len(list(identify_variables(rel.aux_cons[2].body))), 2) + + self.assertTrue(hasattr(rel, 'relaxations')) + self.assertTrue(hasattr(rel.relaxations, 'rel0')) + self.assertTrue(isinstance(rel.relaxations.rel0, coramin.relaxations.PWMcCormickRelaxation)) + self.assertIn(rel.x, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) + self.assertIn(rel.y, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) + self.assertEqual(id(rel.aux_vars[1]), id(rel.relaxations.rel0.get_aux_var())) + + self.assertTrue(hasattr(rel.relaxations, 'rel1')) + self.assertTrue(isinstance(rel.relaxations.rel1, coramin.relaxations.PWUnivariateRelaxation)) + self.assertEqual(id(rel.aux_vars[1]), id(rel.relaxations.rel1._x)) + self.assertEqual(id(rel.aux_vars[2]), id(rel.relaxations.rel1.get_aux_var())) + self.assertTrue(rel.relaxations.rel1.is_rhs_convex()) + self.assertFalse(rel.relaxations.rel1.is_rhs_concave()) + + self.assertFalse(hasattr(rel.relaxations, 'rel2')) + + def test_log(self): + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(1,2)) + m.y = pe.Var(bounds=(1,2)) + m.z = pe.Var() + m.w = pe.Var() + m.c = pe.Constraint(expr=pe.log(m.x*m.y) + m.z == 0) + m.c2 = pe.Constraint(expr=m.w - 3*pe.log(m.x*m.y) == 0) + + rel = coramin.relaxations.relax(m, use_alpha_bb=False) + + self.assertTrue(hasattr(rel, 'aux_cons')) + self.assertTrue(hasattr(rel, 'aux_vars')) + self.assertEqual(len(rel.aux_cons), 2) + self.assertEqual(len(rel.aux_vars), 2) + + self.assertAlmostEqual(rel.aux_vars[1].lb, 1) + self.assertAlmostEqual(rel.aux_vars[1].ub, 4) + + self.assertAlmostEqual(rel.aux_vars[2].lb, math.log(1)) + self.assertAlmostEqual(rel.aux_vars[2].ub, math.log(4)) + + self.assertEqual(rel.aux_cons[1].lower, 0) + self.assertEqual(rel.aux_cons[1].upper, 0) + ders = reverse_sd(rel.aux_cons[1].body) + self.assertEqual(ders[rel.z], 1) + self.assertEqual(ders[rel.aux_vars[2]], 1) + self.assertEqual(len(list(identify_variables(rel.aux_cons[1].body))), 2) + + self.assertEqual(rel.aux_cons[2].lower, 0) + self.assertEqual(rel.aux_cons[2].upper, 0) + ders = reverse_sd(rel.aux_cons[2].body) + self.assertEqual(ders[rel.w], 1) + self.assertEqual(ders[rel.aux_vars[2]], -3) + self.assertEqual(len(list(identify_variables(rel.aux_cons[2].body))), 2) + + self.assertTrue(hasattr(rel, 'relaxations')) + self.assertTrue(hasattr(rel.relaxations, 'rel0')) + self.assertTrue(isinstance(rel.relaxations.rel0, coramin.relaxations.PWMcCormickRelaxation)) + self.assertIn(rel.x, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) + self.assertIn(rel.y, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) + self.assertEqual(id(rel.aux_vars[1]), id(rel.relaxations.rel0.get_aux_var())) + + self.assertTrue(hasattr(rel.relaxations, 'rel1')) + self.assertTrue(isinstance(rel.relaxations.rel1, coramin.relaxations.PWUnivariateRelaxation)) + self.assertIn(rel.aux_vars[1], ComponentSet(rel.relaxations.rel1.get_rhs_vars())) + self.assertEqual(id(rel.aux_vars[2]), id(rel.relaxations.rel1.get_aux_var())) + self.assertFalse(rel.relaxations.rel1.is_rhs_convex()) + self.assertTrue(rel.relaxations.rel1.is_rhs_concave()) + + self.assertFalse(hasattr(rel.relaxations, 'rel2')) + + def test_div1(self): + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(-1, 1)) + m.y = pe.Var(bounds=(1, 2)) + m.z = pe.Var() + m.c = pe.Constraint(expr=m.z - m.x/m.y == 0) + rel = coramin.relaxations.relax(m, in_place=True, use_alpha_bb=False) + self.assertIs(m, rel) + relaxations = list(coramin.relaxations.relaxation_data_objects(m)) + constraints = list(coramin.relaxations.nonrelaxation_component_data_objects(m, ctype=pe.Constraint)) + vars = list(coramin.relaxations.nonrelaxation_component_data_objects(m, ctype=pe.Var)) + self.assertEqual(len(relaxations), 1) + self.assertEqual(len(constraints), 1) + self.assertEqual(len(vars), 4) + r = relaxations[0] + c = constraints[0] + self.assertIsInstance(r, coramin.relaxations.PWMcCormickRelaxationData) + c_vars = ComponentSet(identify_variables(c.body)) + self.assertEqual(len(c_vars), 2) + self.assertEqual(len(rel.aux_vars), 1) + self.assertIn(m.aux_vars[1], c_vars) + self.assertIn(m.z, c_vars) + r_vars = ComponentSet(r.get_rhs_vars()) + self.assertIn(m.y, r_vars) + self.assertIn(m.aux_vars[1], r_vars) + self.assertIs(r.get_aux_var(), m.x) + + def test_div2(self): + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(-1,1)) + m.y = pe.Var(bounds=(-1,1)) + m.z = pe.Var() + m.c = pe.Constraint(expr=m.z - m.x*m.y/2 == 0) + + rel = coramin.relaxations.relax(m, use_alpha_bb=False) + + self.assertTrue(hasattr(rel, 'aux_cons')) + self.assertTrue(hasattr(rel, 'aux_vars')) + self.assertEqual(len(rel.aux_cons), 1) + self.assertEqual(len(rel.aux_vars), 1) + self.assertAlmostEqual(rel.aux_vars[1].lb, -1) + self.assertAlmostEqual(rel.aux_vars[1].ub, 1) + self.assertEqual(rel.aux_cons[1].lower, 0) + self.assertEqual(rel.aux_cons[1].upper, 0) + ders = reverse_sd(rel.aux_cons[1].body) + self.assertEqual(ders[rel.z], 1) + self.assertEqual(ders[rel.aux_vars[1]], -0.5) + self.assertEqual(len(list(identify_variables(rel.aux_cons[1].body))), 2) + + self.assertTrue(hasattr(rel, 'relaxations')) + self.assertTrue(hasattr(rel.relaxations, 'rel0')) + self.assertTrue(isinstance(rel.relaxations.rel0, coramin.relaxations.PWMcCormickRelaxation)) + self.assertIn(rel.x, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) + self.assertIn(rel.y, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) + self.assertEqual(id(rel.aux_vars[1]), id(rel.relaxations.rel0.get_aux_var())) + relaxations = list(coramin.relaxations.relaxation_data_objects(rel)) + self.assertEqual(len(relaxations), 1) + + def test_div3(self): + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(-1, 1)) + m.y = pe.Var(bounds=(1, 2)) + m.z = pe.Var() + m.w = pe.Var() + m.c = pe.Constraint(expr=m.z - m.x/m.y == 0) + m.c2 = pe.Constraint(expr=m.w - m.x/m.y == 0) + rel = coramin.relaxations.relax(m, in_place=True, use_alpha_bb=False) + self.assertIs(m, rel) + relaxations = list(coramin.relaxations.relaxation_data_objects(m)) + constraints = list(coramin.relaxations.nonrelaxation_component_data_objects(m, ctype=pe.Constraint)) + vars = list(coramin.relaxations.nonrelaxation_component_data_objects(m, ctype=pe.Var)) + self.assertEqual(len(relaxations), 1) + self.assertEqual(len(constraints), 2) + self.assertEqual(len(vars), 5) + r = relaxations[0] + c1 = constraints[0] + c2 = constraints[1] + self.assertIsInstance(r, coramin.relaxations.PWMcCormickRelaxationData) + c1_vars = ComponentSet(identify_variables(c1.body)) + c2_vars = ComponentSet(identify_variables(c2.body)) + self.assertEqual(len(c1_vars), 2) + self.assertEqual(len(c2_vars), 2) + self.assertEqual(len(rel.aux_vars), 1) + self.assertIn(m.aux_vars[1], c1_vars) + self.assertIn(m.aux_vars[1], c2_vars) + self.assertTrue(m.z in c1_vars or m.z in c2_vars) + self.assertTrue(m.w in c1_vars or m.w in c2_vars) + r_vars = ComponentSet(r.get_rhs_vars()) + self.assertIn(m.y, r_vars) + self.assertIn(m.aux_vars[1], r_vars) + self.assertIs(r.get_aux_var(), m.x) + + +def _log_of_linear(x): + return pe.log(2*x + 1) + + +def _log_of_polynomial(x): + return pe.log(x**7 + x**5 + x**3 + x) + + +def _x_times_x(x): + return x * x + + +def _quadratic(x): + return x**2 + + +def _sqrt(x): + return x**0.5 + + +def _fractional_exp(x): + return x**1.5 + + +def _variable_exp(x): + return 1.2**x + + +def _cubic(x): + return x**3 + + +def _pow_neg_2(x): + return x**-2 + + +def _pow_neg_3(x): + return x**-3 + + +def _pow_neg_point5(x): + return x**(-0.5) + + +def _pow_neg_1point2(x): + return x**(-1.2) + + +class TestUnivariate(unittest.TestCase): + def helper(self, func, bounds_list): + for relaxation_side in [ + RelaxationSide.UNDER, RelaxationSide.OVER, RelaxationSide.BOTH + ]: + for simplification, use_alpha_bb, eigenvalue_bounder in [ + (True, True, EigenValueBounder.Gershgorin), + (True, True, EigenValueBounder.GershgorinWithSimplification), + (False, True, EigenValueBounder.GershgorinWithSimplification), + (False, False, None), + (True, True, EigenValueBounder.LinearProgram), + ]: + for lb, ub in bounds_list: + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(lb, ub)) + m.aux = pe.Var() + expr = func(m.x) + if relaxation_side == coramin.utils.RelaxationSide.BOTH: + m.c = pe.Constraint(expr=m.aux == expr) + elif relaxation_side == coramin.utils.RelaxationSide.UNDER: + m.c = pe.Constraint(expr=m.aux >= expr) + elif relaxation_side == coramin.utils.RelaxationSide.OVER: + m.c = pe.Constraint(expr=m.aux <= expr) + coramin.relaxations.relax( + m, + in_place=True, + perform_expression_simplification=simplification, + use_alpha_bb=use_alpha_bb, + eigenvalue_bounder=eigenvalue_bounder, + eigenvalue_opt=appsi.solvers.Gurobi(), + ) + opt = appsi.solvers.Gurobi() + all_vars = list(m.component_data_objects(pe.Var, descend_into=True)) + m.obj = pe.Objective(expr=sum(i**2 for i in all_vars)) + + # make sure the original curve is feasible for the relaxation + for _x in [float(i) for i in np.linspace(lb, ub, 10)]: + m.x.fix(_x) + m.aux.fix(pe.value(expr)) + res = opt.solve(m) + self.assertEqual(res.termination_condition, + appsi.base.TerminationCondition.optimal) + if relaxation_side == coramin.utils.RelaxationSide.UNDER: + m.aux.fix(max(pe.value(func(lb)), pe.value(func(ub))) + 1) + res = opt.solve(m) + self.assertEqual(res.termination_condition, + appsi.base.TerminationCondition.optimal) + elif relaxation_side == coramin.utils.RelaxationSide.OVER: + m.aux.fix(min(pe.value(func(lb)), pe.value(func(ub))) - 1) + res = opt.solve(m) + self.assertEqual(res.termination_condition, + appsi.base.TerminationCondition.optimal) + + # ensure the relaxation is exact at the bounds of x + m.aux.unfix() + del m.obj + m.obj = pe.Objective(expr=m.aux) + for _x in [lb, ub]: + m.x.fix(_x) + if relaxation_side in {coramin.utils.RelaxationSide.BOTH, coramin.utils.RelaxationSide.UNDER}: + m.obj.sense = pe.minimize + res = opt.solve(m) + self.assertEqual(res.termination_condition, + appsi.base.TerminationCondition.optimal) + self.assertAlmostEqual(m.aux.value, pe.value(func(_x))) + if relaxation_side in {coramin.utils.RelaxationSide.BOTH, coramin.utils.RelaxationSide.OVER}: + m.obj.sense = pe.maximize + res = opt.solve(m) + self.assertEqual(res.termination_condition, + appsi.base.TerminationCondition.optimal) + self.assertAlmostEqual(m.aux.value, pe.value(func(_x)), 5) + + def test_exp(self): + self.helper(func=pe.exp, bounds_list=[(-1, 1)]) + + def test_log(self): + self.helper(func=pe.log, bounds_list=[(0.5, 1.5)]) + + def test_log10(self): + self.helper(func=pe.log10, bounds_list=[(0.5, 1.5)]) + + def test_log_of_linear(self): + self.helper(func=_log_of_linear, bounds_list=[(0.5, 1.5)]) + + def test_log_of_polynomial(self): + self.helper(func=_log_of_polynomial, bounds_list=[(0.1, 2)]) + + def test_x_times_x(self): + self.helper(func=_x_times_x, bounds_list=[(0.1, 2)]) + + def test_quadratic(self): + self.helper(func=_quadratic, bounds_list=[(-1, 2)]) + + def test_arctan(self): + self.helper(func=pe.atan, bounds_list=[(-1, 1)]) + + def test_sin(self): + self.helper(func=pe.sin, bounds_list=[(-1, 1)]) + + def test_cos(self): + self.helper(func=pe.cos, bounds_list=[(-1, 1)]) + + def test_sqrt(self): + self.helper(func=_sqrt, bounds_list=[(0.5, 2)]) + + def test_sqrt2(self): + self.helper(func=pe.sqrt, bounds_list=[(0.5, 2)]) + + def test_variable_exp(self): + self.helper(func=_variable_exp, bounds_list=[(-2, 3)]) + + def test_cubic(self): + self.helper(func=_cubic, bounds_list=[(-2, 3), (-3, -1), (1, 3)]) + + def test_fractional_exp(self): + self.helper(func=_fractional_exp, bounds_list=[(0.5, 3)]) + + def test_pow_neg2(self): + self.helper(func=_pow_neg_2, bounds_list=[(0.5, 3), (-3, -0.5)]) + + def test_pow_neg3(self): + self.helper(func=_pow_neg_3, bounds_list=[(0.5, 3), (-3, -0.5)]) + + def test_pow_neg_point5(self): + self.helper(func=_pow_neg_point5, bounds_list=[(0.5, 3)]) + + def test_pow_neg_1point2(self): + self.helper(func=_pow_neg_1point2, bounds_list=[(0.5, 3)]) + + +class TestRepeatedTerms(unittest.TestCase): + def helper(self, func, lb, ub): + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(lb, ub)) + m.aux1 = pe.Var() + m.aux2 = pe.Var() + m.c1 = pe.Constraint(expr=m.aux1 <= 2 * func(m.x) + 3) + m.c2 = pe.Constraint(expr=m.aux2 >= 3 * func(m.x) + 2) + coramin.relaxations.relax(m, in_place=True, use_alpha_bb=False) + rels = list(coramin.relaxations.relaxation_data_objects(m)) + self.assertEqual(len(rels), 1) + r = rels[0] + self.assertEqual(r.relaxation_side, coramin.utils.RelaxationSide.BOTH) + + def test_exp(self): + self.helper(func=pe.exp, lb=-1, ub=1) + + def test_log(self): + self.helper(func=pe.log, lb=0.5, ub=1.5) + + def test_log10(self): + self.helper(func=pe.log10, lb=0.5, ub=1.5) + + def test_quadratic(self): + def func(x): + return x**2 + self.helper(func=func, lb=-1, ub=2) + + def test_arctan(self): + self.helper(func=pe.atan, lb=-1, ub=1) + + def test_sin(self): + self.helper(func=pe.sin, lb=-1, ub=1) + + def test_cos(self): + self.helper(func=pe.cos, lb=-1, ub=1) + + +class TestDegree0(unittest.TestCase): + def helper(self, func, param_val): + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(-1, 1)) + m.aux = pe.Var() + m.p = pe.Param(mutable=True, initialize=param_val) + m.c = pe.Constraint(expr=m.aux == func(m.p) * m.x**2) + self.assertIn(m.p, ComponentSet(identify_components(m.c.body, [_ParamData, ScalarParam]))) + coramin.relaxations.relax(m, in_place=True, use_alpha_bb=False) + rels = list(coramin.relaxations.relaxation_data_objects(m)) + self.assertEqual(len(rels), 1) + r = rels[0] + self.assertIsInstance(r, coramin.relaxations.PWXSquaredRelaxationData) + self.assertIn(m.p, ComponentSet(identify_components(m.aux_cons[1].body, [_ParamData, ScalarParam]))) + + def test_exp(self): + self.helper(func=pe.exp, param_val=1) + + def test_log(self): + self.helper(func=pe.log, param_val=1.5) + + def test_log10(self): + self.helper(func=pe.log10, param_val=1.5) + + def test_arctan(self): + self.helper(func=pe.atan, param_val=0.5) + + def test_sin(self): + self.helper(func=pe.sin, param_val=0.5) + + def test_cos(self): + self.helper(func=pe.cos, param_val=0.5) diff --git a/pyomo/contrib/coramin/relaxations/tests/test_copy.py b/pyomo/contrib/coramin/relaxations/tests/test_copy.py new file mode 100644 index 00000000000..a78c73555b5 --- /dev/null +++ b/pyomo/contrib/coramin/relaxations/tests/test_copy.py @@ -0,0 +1,206 @@ +from coramin.relaxations.copy_relaxation import copy_relaxation_with_local_data +import unittest +import pyomo.environ as pe +import coramin +from pyomo.common.collections import ComponentSet +from pyomo.core.expr.sympy_tools import sympyify_expression + + +class TestCopyRelWithLocalData(unittest.TestCase): + def test_quadratic(self): + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(-1, 1)) + m.aux = pe.Var() + m.rel = coramin.relaxations.PWXSquaredRelaxation() + m.rel.build(x=m.x, + aux_var=m.aux, + pw_repn='LOG', + relaxation_side=coramin.utils.RelaxationSide.OVER, + use_linear_relaxation=False) + m2 = pe.ConcreteModel() + m2.x = pe.Var(bounds=(-1, 1)) + m2.aux = pe.Var() + new_rel = copy_relaxation_with_local_data(m.rel, {id(m.x): m2.x, id(m.aux): m2.aux}) + self.assertIn(m2.x, ComponentSet(new_rel.get_rhs_vars())) + self.assertEqual(len(new_rel.get_rhs_vars()), 1) + self.assertEqual(m.rel.use_linear_relaxation, new_rel.use_linear_relaxation) + self.assertEqual(m.rel.relaxation_side, new_rel.relaxation_side) + self.assertIs(m2.aux, new_rel.get_aux_var()) + self.assertEqual(m.rel._pw_repn, new_rel._pw_repn) + self.assertEqual(m.rel._function_shape, new_rel._function_shape) + self.assertIsInstance(new_rel, coramin.relaxations.PWXSquaredRelaxationData) + + def test_arctan(self): + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(-1, 1)) + m.aux = pe.Var() + m.rel = coramin.relaxations.PWArctanRelaxation() + m.rel.build(x=m.x, + aux_var=m.aux, + pw_repn='LOG', + relaxation_side=coramin.utils.RelaxationSide.OVER, + use_linear_relaxation=True) + m2 = pe.ConcreteModel() + m2.x = pe.Var(bounds=(-1, 1)) + m2.aux = pe.Var() + new_rel = copy_relaxation_with_local_data(m.rel, {id(m.x): m2.x, id(m.aux): m2.aux}) + self.assertIn(m2.x, ComponentSet(new_rel.get_rhs_vars())) + self.assertEqual(len(new_rel.get_rhs_vars()), 1) + self.assertEqual(m.rel.use_linear_relaxation, new_rel.use_linear_relaxation) + self.assertEqual(m.rel.relaxation_side, new_rel.relaxation_side) + self.assertIs(m2.aux, new_rel.get_aux_var()) + self.assertEqual(m.rel._pw_repn, new_rel._pw_repn) + self.assertEqual(m.rel._function_shape, new_rel._function_shape) + self.assertIsInstance(new_rel, coramin.relaxations.PWArctanRelaxationData) + + def test_sin(self): + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(-1, 1)) + m.aux = pe.Var() + m.rel = coramin.relaxations.PWSinRelaxation() + m.rel.build(x=m.x, + aux_var=m.aux, + pw_repn='LOG', + relaxation_side=coramin.utils.RelaxationSide.OVER, + use_linear_relaxation=True) + m2 = pe.ConcreteModel() + m2.x = pe.Var(bounds=(-1, 1)) + m2.aux = pe.Var() + new_rel = copy_relaxation_with_local_data(m.rel, {id(m.x): m2.x, id(m.aux): m2.aux}) + self.assertIn(m2.x, ComponentSet(new_rel.get_rhs_vars())) + self.assertEqual(len(new_rel.get_rhs_vars()), 1) + self.assertEqual(m.rel.use_linear_relaxation, new_rel.use_linear_relaxation) + self.assertEqual(m.rel.relaxation_side, new_rel.relaxation_side) + self.assertIs(m2.aux, new_rel.get_aux_var()) + self.assertEqual(m.rel._pw_repn, new_rel._pw_repn) + self.assertEqual(m.rel._function_shape, new_rel._function_shape) + self.assertIsInstance(new_rel, coramin.relaxations.PWSinRelaxationData) + + def test_cos(self): + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(-1, 1)) + m.aux = pe.Var() + m.rel = coramin.relaxations.PWCosRelaxation() + m.rel.build(x=m.x, + aux_var=m.aux, + pw_repn='LOG', + relaxation_side=coramin.utils.RelaxationSide.UNDER, + use_linear_relaxation=True) + m2 = pe.ConcreteModel() + m2.x = pe.Var(bounds=(-1, 1)) + m2.aux = pe.Var() + new_rel = copy_relaxation_with_local_data(m.rel, {id(m.x): m2.x, id(m.aux): m2.aux}) + self.assertIn(m2.x, ComponentSet(new_rel.get_rhs_vars())) + self.assertEqual(len(new_rel.get_rhs_vars()), 1) + self.assertEqual(m.rel.use_linear_relaxation, new_rel.use_linear_relaxation) + self.assertEqual(m.rel.relaxation_side, new_rel.relaxation_side) + self.assertIs(m2.aux, new_rel.get_aux_var()) + self.assertEqual(m.rel._pw_repn, new_rel._pw_repn) + self.assertEqual(m.rel._function_shape, new_rel._function_shape) + self.assertIsInstance(new_rel, coramin.relaxations.PWCosRelaxationData) + + def test_exp(self): + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(-1, 1)) + m.aux = pe.Var() + m.rel = coramin.relaxations.PWUnivariateRelaxation() + m.rel.build(x=m.x, + aux_var=m.aux, + shape=coramin.utils.FunctionShape.CONVEX, + f_x_expr=pe.exp(m.x), + pw_repn='LOG', + relaxation_side=coramin.utils.RelaxationSide.UNDER, + use_linear_relaxation=True) + m2 = pe.ConcreteModel() + m2.x = pe.Var(bounds=(-1, 1)) + m2.aux = pe.Var() + new_rel = copy_relaxation_with_local_data(m.rel, {id(m.x): m2.x, id(m.aux): m2.aux}) + self.assertIn(m2.x, ComponentSet(new_rel.get_rhs_vars())) + self.assertEqual(len(new_rel.get_rhs_vars()), 1) + self.assertEqual(m.rel.use_linear_relaxation, new_rel.use_linear_relaxation) + self.assertEqual(m.rel.relaxation_side, new_rel.relaxation_side) + self.assertIs(m2.aux, new_rel.get_aux_var()) + self.assertEqual(m.rel._pw_repn, new_rel._pw_repn) + self.assertEqual(m.rel._function_shape, new_rel._function_shape) + self.assertIsInstance(new_rel, coramin.relaxations.PWUnivariateRelaxationData) + + def test_log(self): + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(0.5, 1.5)) + m.aux = pe.Var() + m.rel = coramin.relaxations.PWUnivariateRelaxation() + m.rel.build(x=m.x, + aux_var=m.aux, + shape=coramin.utils.FunctionShape.CONCAVE, + f_x_expr=pe.log(m.x), + pw_repn='LOG', + relaxation_side=coramin.utils.RelaxationSide.UNDER, + use_linear_relaxation=True) + m2 = pe.ConcreteModel() + m2.x = pe.Var(bounds=(0.5, 1.5)) + m2.aux = pe.Var() + new_rel = copy_relaxation_with_local_data(m.rel, {id(m.x): m2.x, id(m.aux): m2.aux}) + self.assertIn(m2.x, ComponentSet(new_rel.get_rhs_vars())) + self.assertEqual(len(new_rel.get_rhs_vars()), 1) + self.assertEqual(m.rel.use_linear_relaxation, new_rel.use_linear_relaxation) + self.assertEqual(m.rel.relaxation_side, new_rel.relaxation_side) + self.assertIs(m2.aux, new_rel.get_aux_var()) + self.assertEqual(m.rel._pw_repn, new_rel._pw_repn) + self.assertEqual(m.rel._function_shape, new_rel._function_shape) + self.assertIsInstance(new_rel, coramin.relaxations.PWUnivariateRelaxationData) + + def test_multivariate(self): + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(-1, 1)) + m.y = pe.Var(bounds=(-1, 1)) + m.aux = pe.Var() + m.rel = coramin.relaxations.MultivariateRelaxation() + m.rel.build(aux_var=m.aux, + shape=coramin.utils.FunctionShape.CONVEX, + f_x_expr=(m.x**2 + m.y**2), + use_linear_relaxation=True) + m2 = pe.ConcreteModel() + m2.x = pe.Var(bounds=(-1, 1)) + m2.y = pe.Var(bounds=(-1, 1)) + m2.aux = pe.Var() + new_rel = copy_relaxation_with_local_data(m.rel, {id(m.x): m2.x, + id(m.y): m2.y, + id(m.aux): m2.aux}) + self.assertEqual(m.rel.use_linear_relaxation, new_rel.use_linear_relaxation) + self.assertEqual(m.rel.relaxation_side, new_rel.relaxation_side) + rhs_vars = ComponentSet(new_rel.get_rhs_vars()) + self.assertIn(m2.x, rhs_vars) + self.assertIn(m2.y, rhs_vars) + self.assertEqual(len(rhs_vars), 2) + self.assertIs(m2.aux, new_rel.get_aux_var()) + self.assertEqual(m.rel._function_shape, new_rel._function_shape) + self.assertIsInstance(new_rel, coramin.relaxations.MultivariateRelaxationData) + self.assertEqual(sympyify_expression(m2.x**2 + m2.y**2 - new_rel.get_rhs_expr())[1], 0) + + def test_multivariate2(self): + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(-1, 1)) + m.y = pe.Var(bounds=(-1, 1)) + m.aux = pe.Var() + m.rel = coramin.relaxations.MultivariateRelaxation() + m.rel.build(aux_var=m.aux, + shape=coramin.utils.FunctionShape.CONCAVE, + f_x_expr=(-m.x**2 - m.y**2), + use_linear_relaxation=True) + m2 = pe.ConcreteModel() + m2.x = pe.Var(bounds=(-1, 1)) + m2.y = pe.Var(bounds=(-1, 1)) + m2.aux = pe.Var() + new_rel = copy_relaxation_with_local_data(m.rel, {id(m.x): m2.x, + id(m.y): m2.y, + id(m.aux): m2.aux}) + self.assertEqual(m.rel.use_linear_relaxation, new_rel.use_linear_relaxation) + self.assertEqual(m.rel.relaxation_side, new_rel.relaxation_side) + rhs_vars = ComponentSet(new_rel.get_rhs_vars()) + self.assertIn(m2.x, rhs_vars) + self.assertIn(m2.y, rhs_vars) + self.assertEqual(len(rhs_vars), 2) + self.assertIs(m2.aux, new_rel.get_aux_var()) + self.assertEqual(m.rel._function_shape, new_rel._function_shape) + self.assertIsInstance(new_rel, coramin.relaxations.MultivariateRelaxationData) + self.assertEqual(sympyify_expression(-m2.x**2 - m2.y**2 - new_rel.get_rhs_expr())[1], 0) diff --git a/pyomo/contrib/coramin/relaxations/tests/test_iterators.py b/pyomo/contrib/coramin/relaxations/tests/test_iterators.py new file mode 100644 index 00000000000..a1d2417c4f5 --- /dev/null +++ b/pyomo/contrib/coramin/relaxations/tests/test_iterators.py @@ -0,0 +1,87 @@ +import coramin +import unittest +import pyomo.environ as pe +from pyomo.common.collections import ComponentSet + + +class TestIterators(unittest.TestCase): + def setUp(self): + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(0.5, 1.5)) + m.y = pe.Var() + m.c1 = pe.Constraint(expr=m.y == m.x) + m.r1 = coramin.relaxations.PWUnivariateRelaxation() + m.r1.set_input(x=m.x, + aux_var=m.y, + shape=coramin.utils.FunctionShape.CONCAVE, + f_x_expr=pe.log(m.x)) + m.r1.add_partition_point(value=1) + m.r1.rebuild() + m.b1 = pe.Block() + m.b1.x = pe.Var(bounds=(0.5, 1.5)) + m.b1.y = pe.Var() + m.b1.c1 = pe.Constraint(expr=m.b1.y == m.b1.x) + m.b1.r1 = coramin.relaxations.PWUnivariateRelaxation() + m.b1.r1.set_input(x=m.b1.x, + aux_var=m.b1.y, + shape=coramin.utils.FunctionShape.CONCAVE, + f_x_expr=pe.log(m.b1.x)) + m.b1.r1.add_partition_point(value=1) + m.b1.r1.rebuild() + m.b1.b1 = pe.Block() + + self.m = m + + def test_relaxation_data_objects(self): + m = self.m + rels = list(coramin.relaxations.relaxation_data_objects(m, descend_into=True, active=True)) + self.assertEqual(len(rels), 2) + self.assertIn(m.r1, rels) + self.assertIn(m.b1.r1, rels) + + m.r1.deactivate() + rels = list(coramin.relaxations.relaxation_data_objects(m, descend_into=True, active=True)) + self.assertEqual(len(rels), 1) + self.assertNotIn(m.r1, rels) + self.assertIn(m.b1.r1, rels) + + rels = list(coramin.relaxations.relaxation_data_objects(m, descend_into=True, active=None)) + self.assertEqual(len(rels), 2) + self.assertIn(m.r1, rels) + self.assertIn(m.b1.r1, rels) + + m.r1.activate() + rels = list(coramin.relaxations.relaxation_data_objects(m, descend_into=False)) + self.assertEqual(len(rels), 1) + self.assertIn(m.r1, rels) + self.assertNotIn(m.b1.r1, rels) + + def test_nonrelaxation_component_data_objects(self): + m = self.m + all_vars = list(m.component_data_objects(pe.Var, descend_into=True)) + non_relaxation_vars = list(coramin.relaxations.nonrelaxation_component_data_objects(m, + ctype=pe.Var, + descend_into=True)) + self.assertEqual(len(non_relaxation_vars), 4) + self.assertGreater(len(all_vars), 4) + + all_vars = list(m.component_data_objects(pe.Var, descend_into=False)) + non_relaxation_vars = list(coramin.relaxations.nonrelaxation_component_data_objects(m, + ctype=pe.Var, + descend_into=False)) + self.assertEqual(len(non_relaxation_vars), 2) + self.assertEqual(len(all_vars), 2) + + all_blocks = list(m.component_data_objects(pe.Block, descend_into=True)) + non_relaxation_blocks = list(coramin.relaxations.nonrelaxation_component_data_objects(m, + ctype=pe.Block, + descend_into=True)) + self.assertEqual(len(non_relaxation_blocks), 2) + self.assertEqual(len(all_blocks), 8) + + all_blocks = list(m.component_data_objects(pe.Block, descend_into=False)) + non_relaxation_blocks = list(coramin.relaxations.nonrelaxation_component_data_objects(m, + ctype=pe.Block, + descend_into=False)) + self.assertEqual(len(non_relaxation_blocks), 1) + self.assertEqual(len(all_blocks), 2) diff --git a/pyomo/contrib/coramin/relaxations/tests/test_mccormick.py b/pyomo/contrib/coramin/relaxations/tests/test_mccormick.py new file mode 100644 index 00000000000..053bdf80bb7 --- /dev/null +++ b/pyomo/contrib/coramin/relaxations/tests/test_mccormick.py @@ -0,0 +1,102 @@ +import pyomo.environ as pyo +import unittest +import coramin + + +class TestMcCormick(unittest.TestCase): + @classmethod + def setUpClass(cls): + pass + + @classmethod + def tearDownClass(cls): + pass + + def test_mccormick1(self): + model = pyo.ConcreteModel() + model.x = pyo.Var(bounds=(0, 6)) + model.y = pyo.Var(bounds=(0, 3)) + model.w = pyo.Var() + + model.obj = pyo.Objective(expr=-model.w - 2 * model.x) + model.con = pyo.Constraint(expr=model.w <= 12) + model.mc = coramin.relaxations.PWMcCormickRelaxation() + model.mc.build(x1=model.x, x2=model.y, aux_var=model.w) + + linsolver = pyo.SolverFactory('gurobi_direct') + linsolver.solve(model) + self.assertAlmostEqual(pyo.value(model.x), 6.0, 6) + self.assertAlmostEqual(pyo.value(model.y), 2.0, 6) + + def test_mccormick2(self): + model = pyo.ConcreteModel() + model.x = pyo.Var(bounds=(0, 6)) + model.y = pyo.Var(bounds=(0, 3)) + model.w = pyo.Var() + + model.obj = pyo.Objective(expr=-model.w - 2 * model.x) + model.con = pyo.Constraint(expr=model.w <= 12) + def mc_rule(b): + b.build(x1=model.x, x2=model.y, aux_var=model.w) + model.mc = coramin.relaxations.PWMcCormickRelaxation(rule=mc_rule) + + linsolver = pyo.SolverFactory('gurobi_direct') + linsolver.solve(model) + self.assertAlmostEqual(pyo.value(model.x), 6.0, 6) + self.assertAlmostEqual(pyo.value(model.y), 2.0, 6) + + def test_mccormick3_BOTH(self): + model = pyo.ConcreteModel() + model.x = pyo.Var(bounds=(0, 6)) + model.y = pyo.Var(bounds=(0, 3)) + model.w = pyo.Var() + + model.obj = pyo.Objective(expr=-model.w - 2 * model.x) + model.con = pyo.Constraint(expr=model.w <= 12) + + def mc_rule(b): + m = b.parent_block() + b.build(x1=m.x, x2=m.y, aux_var=m.w) + model.mc = coramin.relaxations.PWMcCormickRelaxation(rule=mc_rule) + + linsolver = pyo.SolverFactory('gurobi_direct', tee=True) + linsolver.solve(model) + self.assertAlmostEqual(pyo.value(model.x), 6.0, 6) + self.assertAlmostEqual(pyo.value(model.y), 2.0, 6) + + def test_mccormick3_OVER(self): + model = pyo.ConcreteModel() + model.x = pyo.Var(bounds=(0, 6)) + model.y = pyo.Var(bounds=(0, 3)) + model.w = pyo.Var() + + model.obj = pyo.Objective(expr=-model.w + 0.1*model.x + 0.1*model.y) + model.con = pyo.Constraint(expr=model.w <= 12) + + def mc_rule(b): + m = b.parent_block() + b.build(x1=m.x, x2=m.y, aux_var=m.w, relaxation_side=coramin.utils.RelaxationSide.OVER) + model.mc = coramin.relaxations.PWMcCormickRelaxation(rule=mc_rule) + + linsolver = pyo.SolverFactory('gurobi_direct') + linsolver.solve(model) + self.assertAlmostEqual(pyo.value(model.x), 4.0, 6) + self.assertAlmostEqual(pyo.value(model.y), 2.0, 6) + + def test_mccormick3_UNDER(self): + model = pyo.ConcreteModel() + model.x = pyo.Var(bounds=(0, 6)) + model.y = pyo.Var(bounds=(0, 3)) + model.w = pyo.Var() + + model.obj = pyo.Objective(expr=-model.w - 2 * model.x) + model.con = pyo.Constraint(expr=model.w <= 12) + + def mc_rule(b): + m = b.parent_block() + b.build(x1=m.x, x2=m.y, aux_var=m.w, relaxation_side=coramin.utils.RelaxationSide.UNDER) + model.mc = coramin.relaxations.PWMcCormickRelaxation(rule=mc_rule) + + linsolver = pyo.SolverFactory('gurobi_direct', tee=True) + linsolver.solve(model) + self.assertAlmostEqual(pyo.value(model.w), 12.0, 6) diff --git a/pyomo/contrib/coramin/relaxations/tests/test_relaxations.py b/pyomo/contrib/coramin/relaxations/tests/test_relaxations.py new file mode 100644 index 00000000000..db3ad094432 --- /dev/null +++ b/pyomo/contrib/coramin/relaxations/tests/test_relaxations.py @@ -0,0 +1,951 @@ +import unittest +import pyomo.environ as pe +import coramin +from pyomo.core.base.block import _BlockData +from pyomo.core.base.var import _GeneralVarData +from pyomo.core.expr.numeric_expr import ExpressionBase +from typing import Sequence, List, Tuple +import numpy as np +import itertools +from pyomo.contrib import appsi +from pyomo.core.expr.visitor import identify_variables +from pyomo.common.collections import ComponentSet +from pyomo.core.expr.compare import compare_expressions +from pyomo.repn.standard_repn import generate_standard_repn +from pyomo.util.report_scaling import _check_coefficients +from pyomo.core.expr.calculus.derivatives import differentiate, Modes, reverse_sd +from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr +from pyomo.core.expr import sympy_tools +import io + + +def _grid_rhs_vars(v_list: Sequence[_GeneralVarData], num_points: int = 30) -> List[Tuple[float, ...]]: + res = list() + for v in v_list: + res.append(np.linspace(v.lb, v.ub, num_points)) + res = list(tuple(float(p) for p in i) for i in itertools.product(*res)) + return res + + +def _get_rhs_vals(rhs_vars: Sequence[_GeneralVarData], + rhs_expr: ExpressionBase, + eval_pts: List[Tuple[float, ...]]) -> List[float]: + rhs_vals = list() + for pt in eval_pts: + for v, p in zip(rhs_vars, pt): + v.fix(p) + rhs_vals.append(pe.value(rhs_expr)) + for v in rhs_vars: + v.unfix() + return rhs_vals + + +def _get_relaxation_vals(rhs_vars: Sequence[_GeneralVarData], + rhs_expr: ExpressionBase, + m: _BlockData, + rel: coramin.relaxations.BaseRelaxationData, + eval_pts: List[Tuple[float, ...]], + rel_side: coramin.utils.RelaxationSide, + linear: bool = True) -> List[float]: + opt = appsi.solvers.Gurobi() + opt.update_config.update_vars = True + opt.update_config.check_for_new_or_removed_vars = False + opt.update_config.check_for_new_or_removed_constraints = False + opt.update_config.check_for_new_or_removed_params = False + opt.update_config.update_constraints = False + opt.update_config.update_params = False + opt.update_config.update_named_expressions = False + opt.update_config.check_for_new_objective = False + opt.update_config.update_objective = False + if linear: + opt.update_config.treat_fixed_vars_as_params = False + + if rel_side == coramin.utils.RelaxationSide.UNDER: + sense = pe.minimize + else: + sense = pe.maximize + m.obj = pe.Objective(expr=rel.get_aux_var(), sense=sense) + + under_est_vals = list() + for pt in eval_pts: + for v, p in zip(rhs_vars, pt): + v.fix(p) + res = opt.solve(m) + assert res.termination_condition == appsi.base.TerminationCondition.optimal + under_est_vals.append(rel.get_aux_var().value) + + del m.obj + for v in rhs_vars: + v.unfix() + return under_est_vals + + +def _num_cons(rel): + cons = list(rel.component_data_objects(pe.Constraint, descend_into=True, active=True)) + return len(cons) + + +def _check_unbounded(m: _BlockData, + rel: coramin.relaxations.BaseRelaxationData, + rel_side: coramin.utils.RelaxationSide, + linear: bool = True): + if rel_side == coramin.utils.RelaxationSide.UNDER: + sense = pe.minimize + else: + sense = pe.maximize + m.obj = pe.Objective(expr=rel.get_aux_var(), sense=sense) + + for v in rel.get_rhs_vars(): + if v.has_lb() and v.has_ub(): + v.fix(0.5*(v.lb + v.ub)) + elif v.has_lb(): + v.fix(v.lb + 0.1) + elif v.has_ub(): + v.fix(v.ub - 0.1) + else: + v.fix(1) + + opt = appsi.solvers.Gurobi() + opt.gurobi_options['DualReductions'] = 0 + opt.config.load_solution = False + res = opt.solve(m) + + del m.obj + + return res.termination_condition == appsi.base.TerminationCondition.unbounded + + +def _check_linear(m: _BlockData): + for c in m.component_data_objects(pe.Constraint, descend_into=True, active=True): + repn = generate_standard_repn(c.body) + if not repn.is_linear(): + return False + return True + + +def _check_linear_or_convex(rel: coramin.relaxations.BaseRelaxationData): + for c in rel.component_data_objects(pe.Constraint, descend_into=True, active=True): + repn = generate_standard_repn(c.body, quadratic=False) + if repn.is_linear(): + continue + + if c.lower is not None and c.upper is not None: + return False # nonlinear equality constraints are not convex + + # reconstruct the expression without the aux_var + e = repn.constant + for coef, v in zip(repn.linear_coefs, repn.linear_vars): + if v is not rel.get_aux_var(): + e += coef*v + e += repn.nonlinear_expr + + # this will only work if all the off-diagonal elements of the hessian are 0 + rhs_vars = rel.get_rhs_vars() + ders = reverse_sd(e) + for v1 in rhs_vars: + v1_der = ders[v1] + hes = reverse_sd(v1_der) + for v2 in rhs_vars: + if v2 is not v1: + assert v2 not in hes + hes = differentiate(v1_der, wrt=v1, mode=Modes.reverse_symbolic) + if type(hes) not in {int, float}: + om, se = sympy_tools.sympyify_expression(hes) + se = se.simplify() + hes = sympy_tools.sympy2pyomo_expression(se, om) + hes_lb, hes_ub = compute_bounds_on_expr(hes) + if c.lower is not None: + if hes_ub > 0: + return False + else: + assert c.upper is not None + if hes_lb < 0: + return False + + return True + + +def _check_scaling(m: _BlockData, rel: coramin.relaxations.BaseRelaxationData) -> bool: + cons_with_large_coefs = dict() + cons_with_small_coefs = dict() + for c in m.component_data_objects(pe.Constraint, descend_into=True, active=True): + _check_coefficients(c, c.body, rel.large_coef, rel.small_coef, cons_with_large_coefs, cons_with_small_coefs) + passed = len(cons_with_large_coefs) == 0 and len(cons_with_small_coefs) == 0 + return passed + + +class TestRelaxationBasics(unittest.TestCase): + def valid_relaxation_helper(self, m: _BlockData, rel: coramin.relaxations.BaseRelaxationData, + rhs_expr: ExpressionBase, num_points: int = 30, check_underestimator: bool = True, + check_overestimator: bool = True): + if rel.use_linear_relaxation: + self.assertTrue(_check_linear(m)) + rhs_vars = rel.get_rhs_vars() + sample_points = _grid_rhs_vars(rhs_vars, num_points=num_points) + rhs_vals = _get_rhs_vals(rhs_vars, rhs_expr, sample_points) + rhs_vals = np.array(rhs_vals) + + if check_underestimator: + under_est_vals = _get_relaxation_vals(rhs_vars, rhs_expr, m, rel, sample_points, + coramin.utils.RelaxationSide.UNDER) + under_est_vals = np.array(under_est_vals) + self.assertTrue(np.all(rhs_vals >= under_est_vals)) + if check_overestimator: + over_est_vals = _get_relaxation_vals(rhs_vars, rhs_expr, m, rel, sample_points, + coramin.utils.RelaxationSide.OVER) + over_est_vals = np.array(over_est_vals) + self.assertTrue(np.all(rhs_vals <= over_est_vals)) + + def equal_at_points_helper(self, m: _BlockData, rel: coramin.relaxations.BaseRelaxationData, + rhs_expr: ExpressionBase, pts: Sequence[Tuple[float, ...]], + check_underestimator: bool = True, check_overestimator: bool = True, + linear: bool = True): + rhs_vars = rel.get_rhs_vars() + rhs_vals = _get_rhs_vals(rhs_vars, rhs_expr, pts) + rhs_vals = np.array(rhs_vals) + if check_underestimator: + under_est_vals = _get_relaxation_vals(rhs_vars, rhs_expr, m, rel, pts, + coramin.utils.RelaxationSide.UNDER, linear) + under_est_vals = np.array(under_est_vals) + self.assertTrue(np.all(np.isclose(rhs_vals, under_est_vals))) + if check_overestimator: + over_est_vals = _get_relaxation_vals(rhs_vars, rhs_expr, m, rel, pts, + coramin.utils.RelaxationSide.OVER, linear) + over_est_vals = np.array(over_est_vals) + self.assertTrue(np.all(np.isclose(rhs_vals, over_est_vals))) + + def nonlinear_relaxation_helper(self, m: _BlockData, rel: coramin.relaxations.BaseRelaxationData, + rhs_expr: ExpressionBase, num_points: int = 30, + supports_underestimator: bool = True, supports_overestimator: bool = True, + check_equal_at_points: bool = True): + rel.use_linear_relaxation = False + rel.rebuild() + if rel.is_rhs_convex() or rel.is_rhs_concave(): + self.assertFalse(_check_linear(m)) + self.assertTrue(_check_linear_or_convex(rel)) + else: + self.assertTrue(_check_linear(m)) + rhs_vars = rel.get_rhs_vars() + sample_points = _grid_rhs_vars(rhs_vars, num_points=num_points) + rhs_vals = _get_rhs_vals(rhs_vars, rhs_expr, sample_points) + rhs_vals = np.array(rhs_vals) + + if supports_underestimator: + under_est_vals = _get_relaxation_vals(rhs_vars, rhs_expr, m, rel, sample_points, + coramin.utils.RelaxationSide.UNDER, linear=False) + under_est_vals = np.array(under_est_vals) + if rel.is_rhs_convex() and check_equal_at_points: + self.assertTrue(np.all(np.isclose(rhs_vals, under_est_vals))) + else: + self.assertTrue(np.all(rhs_vals >= under_est_vals)) + if supports_overestimator: + over_est_vals = _get_relaxation_vals(rhs_vars, rhs_expr, m, rel, sample_points, + coramin.utils.RelaxationSide.OVER, linear=False) + over_est_vals = np.array(over_est_vals) + if rel.is_rhs_concave() and check_equal_at_points: + self.assertTrue(np.all(np.isclose(rhs_vals, over_est_vals))) + else: + self.assertTrue(np.all(rhs_vals <= over_est_vals)) + + if supports_underestimator and supports_overestimator: + orig_relaxation_side = rel.relaxation_side + if rel.is_rhs_convex(): + rel.relaxation_side = coramin.utils.RelaxationSide.OVER + if rel.is_rhs_concave(): + rel.relaxation_side = coramin.utils.RelaxationSide.UNDER + rel.rebuild() + self.assertTrue(_check_linear(m)) + rel.relaxation_side = orig_relaxation_side + + rel.use_linear_relaxation = True + rel.rebuild() + + def original_constraint_helper(self, m: _BlockData, rel: coramin.relaxations.BaseRelaxationData, + rhs_expr: ExpressionBase, num_points: int = 15, supports_underestimator: bool = True, + supports_overestimator: bool = True): + rel.rebuild(build_nonlinear_constraint=True) + self.assertFalse(_check_linear(m)) + rhs_vars = rel.get_rhs_vars() + sample_points = _grid_rhs_vars(rhs_vars, num_points) + rhs_vals = _get_rhs_vals(rhs_vars, rhs_expr, sample_points) + rhs_vals = np.array(rhs_vals) + + if supports_underestimator: + under_est_vals = _get_relaxation_vals(rhs_vars, rhs_expr, m, rel, sample_points, + coramin.utils.RelaxationSide.UNDER, linear=False) + under_est_vals = np.array(under_est_vals) + self.assertTrue(np.all(np.isclose(rhs_vals, under_est_vals))) + if supports_overestimator: + over_est_vals = _get_relaxation_vals(rhs_vars, rhs_expr, m, rel, sample_points, + coramin.utils.RelaxationSide.OVER, linear=False) + over_est_vals = np.array(over_est_vals) + self.assertTrue(np.all(np.isclose(rhs_vals, over_est_vals))) + + rel.rebuild() + self.valid_relaxation_helper(m, rel, rhs_expr, num_points, supports_underestimator, supports_overestimator) + + def relaxation_side_helper(self, m: _BlockData, rel: coramin.relaxations.BaseRelaxationData, + rhs_expr: ExpressionBase, check_nonlinear_relaxation: bool = True): + rel.relaxation_side = coramin.utils.RelaxationSide.UNDER + rel.rebuild() + sample_points = [tuple(v.lb for v in rel.get_rhs_vars()), tuple(v.ub for v in rel.get_rhs_vars())] + self.equal_at_points_helper(m, rel, rhs_expr, sample_points, True, False) + self.assertTrue(_check_unbounded(m, rel, coramin.RelaxationSide.OVER)) + + rel.relaxation_side = coramin.utils.RelaxationSide.OVER + rel.rebuild() + self.equal_at_points_helper(m, rel, rhs_expr, sample_points, False, True) + self.assertTrue(_check_unbounded(m, rel, coramin.RelaxationSide.UNDER)) + + if check_nonlinear_relaxation: + rel.use_linear_relaxation = False + + rel.relaxation_side = coramin.utils.RelaxationSide.UNDER + rel.rebuild() + sample_points = [(v.lb, v.ub) for v in rel.get_rhs_vars()] + self.equal_at_points_helper(m, rel, rhs_expr, sample_points, True, False, False) + self.assertTrue(_check_unbounded(m, rel, coramin.RelaxationSide.OVER, False)) + + rel.relaxation_side = coramin.utils.RelaxationSide.OVER + rel.rebuild() + self.equal_at_points_helper(m, rel, rhs_expr, sample_points, False, True, False) + self.assertTrue(_check_unbounded(m, rel, coramin.RelaxationSide.UNDER, False)) + + rel.relaxation_side = coramin.utils.RelaxationSide.UNDER + rel.rebuild(build_nonlinear_constraint=True) + sample_points = [(v.lb, v.ub) for v in rel.get_rhs_vars()] + self.equal_at_points_helper(m, rel, rhs_expr, sample_points, True, False, False) + self.assertTrue(_check_unbounded(m, rel, coramin.RelaxationSide.OVER, False)) + + rel.relaxation_side = coramin.utils.RelaxationSide.OVER + rel.rebuild(build_nonlinear_constraint=True) + self.equal_at_points_helper(m, rel, rhs_expr, sample_points, False, True, False) + self.assertTrue(_check_unbounded(m, rel, coramin.RelaxationSide.UNDER, False)) + + rel.use_linear_relaxation = True + rel.relaxation_side = coramin.RelaxationSide.BOTH + rel.rebuild() + + def changing_bounds_helper(self, m: _BlockData, rel: coramin.relaxations.BaseRelaxationData, + rhs_expr: ExpressionBase, num_points: int = 10, supports_underestimator: bool = True, + supports_overestimator: bool = True, check_equal_at_points: bool = True): + rhs_vars = rel.get_rhs_vars() + orig_bnds = pe.ComponentMap((v, (v.lb, v.ub)) for v in rhs_vars) + grid_pts = _grid_rhs_vars(rhs_vars, num_points=num_points) + for pt in grid_pts: + for v, p in zip(rhs_vars, pt): + v.setlb(p) + rel.rebuild() + self.assertLessEqual(_num_cons(rel), 4) + self.valid_relaxation_helper(m, rel, rhs_expr, num_points, supports_underestimator, supports_overestimator) + self.equal_at_points_helper(m, rel, rhs_expr, + [tuple(v.lb for v in rhs_vars), tuple(v.ub for v in rhs_vars)], + supports_underestimator, supports_overestimator) + if rel.is_rhs_convex() or rel.is_rhs_concave(): + self.nonlinear_relaxation_helper(m, rel, rhs_expr, num_points, + supports_underestimator, supports_overestimator, + check_equal_at_points) + for v, (v_lb, v_ub) in orig_bnds.items(): + v.setlb(v_lb) + v.setub(v_ub) + rel.rebuild() + self.assertLessEqual(_num_cons(rel), 4) + self.valid_relaxation_helper(m, rel, rhs_expr, num_points, supports_underestimator, supports_overestimator) + self.equal_at_points_helper(m, rel, rhs_expr, [tuple(v.lb for v in rhs_vars), tuple(v.ub for v in rhs_vars)], + supports_underestimator, supports_overestimator) + for pt in grid_pts: + for v, p in zip(rhs_vars, pt): + v.setub(p) + rel.rebuild() + self.assertLessEqual(_num_cons(rel), 4) + self.valid_relaxation_helper(m, rel, rhs_expr, num_points, + supports_underestimator, supports_overestimator) + self.equal_at_points_helper(m, rel, rhs_expr, + [tuple(v.lb for v in rhs_vars), tuple(v.ub for v in rhs_vars)], + supports_underestimator, supports_overestimator) + if rel.is_rhs_convex() or rel.is_rhs_concave(): + self.nonlinear_relaxation_helper(m, rel, rhs_expr, num_points, + supports_underestimator, supports_overestimator, + check_equal_at_points) + for v, (v_lb, v_ub) in orig_bnds.items(): + v.setlb(v_lb) + v.setub(v_ub) + rel.rebuild() + self.assertLessEqual(_num_cons(rel), 4) + self.valid_relaxation_helper(m, rel, rhs_expr, num_points, supports_underestimator, supports_overestimator) + self.equal_at_points_helper(m, rel, rhs_expr, [tuple(v.lb for v in rhs_vars), tuple(v.ub for v in rhs_vars)], + supports_underestimator, supports_overestimator) + + def large_bounds_helper(self, m: _BlockData, rel: coramin.relaxations.BaseRelaxationData, lb=1, ub=1e6): + orig_bnds = pe.ComponentMap((v, (v.lb, v.ub)) for v in rel.get_rhs_vars()) + + for v in rel.get_rhs_vars(): + v.setlb(lb) + v.setub(ub) + rel.rebuild() + + scaling_passed = _check_scaling(m, rel) + self.assertTrue(scaling_passed) + + if rel.is_rhs_convex(): + self.assertTrue(_check_unbounded(m, rel, coramin.utils.RelaxationSide.OVER)) + elif rel.is_rhs_concave(): + self.assertTrue(_check_unbounded(m, rel, coramin.utils.RelaxationSide.UNDER)) + else: + self.assertTrue(_check_unbounded(m, rel, coramin.utils.RelaxationSide.UNDER)) + self.assertTrue(_check_unbounded(m, rel, coramin.utils.RelaxationSide.OVER)) + + for v, (v_lb, v_ub) in orig_bnds.items(): + v.setlb(v_lb) + v.setub(v_ub) + rel.rebuild() + + def infinite_bounds_helper(self, m: _BlockData, rel: coramin.relaxations.BaseRelaxationData): + self.large_bounds_helper(m, rel, None, None) + self.large_bounds_helper(m, rel, ub=None) + self.large_bounds_helper(m, rel, lb=None) + + def oa_cuts_helper(self, m: _BlockData, rel: coramin.relaxations.BaseRelaxationData, rhs_expr: ExpressionBase, + num_pts: int = 30, supports_underestimator: bool = True, supports_overestimator: bool = True, + check_equal_at_points: bool = True): + rhs_vars = rel.get_rhs_vars() + sample_points = _grid_rhs_vars(rhs_vars, 5) + for pt in sample_points: + rel.add_oa_point(pt) + rel.rebuild() + if rel.is_rhs_convex() or rel.is_rhs_concave(): + self.assertEqual(len(rel._cuts), len(sample_points)) + self.valid_relaxation_helper(m, rel, rhs_expr, num_pts, supports_underestimator, supports_overestimator) + if rel.is_rhs_convex(): + check_under = True + else: + check_under = False + if rel.is_rhs_concave(): + check_over = True + else: + check_over = False + if check_equal_at_points: + self.equal_at_points_helper(m, rel, rhs_expr, sample_points, check_under, check_over) + rel.push_oa_points('foo') + rel.clear_oa_points() + rel.rebuild() + if rel.has_convex_underestimator() or rel.has_concave_overestimator(): + self.assertEqual(len(rel._cuts), 2) + else: + self.assertIsNone(rel._cuts) + rel.pop_oa_points('foo') + rel.rebuild() + if rel.is_rhs_convex() or rel.is_rhs_concave(): + self.assertEqual(len(rel._cuts), len(sample_points)) + if check_equal_at_points: + self.equal_at_points_helper(m, rel, rhs_expr, sample_points, check_under, check_over) + rel.clear_oa_points() + rel.rebuild() + + def add_cuts_helper(self, m: _BlockData, rel: coramin.relaxations.BaseRelaxationData, rhs_expr: ExpressionBase, + num_pts: int = 30, supports_underestimator: bool = True, supports_overestimator: bool = True, + check_equal_at_points: bool = True): + rhs_vars = rel.get_rhs_vars() + sample_points = _grid_rhs_vars(rhs_vars, 5) + for keep_cut in [True, False]: + for offset in [-10, 10]: + for pt in sample_points: + for v, p in zip(rhs_vars, pt): + v.value = p + rel.get_aux_var().value = pe.value(rhs_expr) + offset + rel.add_cut(keep_cut=keep_cut, check_violation=True) + self.valid_relaxation_helper(m, rel, rhs_expr, num_pts, supports_underestimator, supports_overestimator) + if rel.has_convex_underestimator(): + if offset < 0: + self.assertEqual(len(rel._cuts), len(sample_points)) + if check_equal_at_points: + self.equal_at_points_helper(m, rel, rhs_expr, sample_points, True, False) + else: + self.assertEqual(len(rel._cuts), 2) + if rel.has_concave_overestimator(): + if offset > 0: + self.assertEqual(len(rel._cuts), len(sample_points)) + if check_equal_at_points: + self.equal_at_points_helper(m, rel, rhs_expr, sample_points, False, True) + else: + self.assertEqual(len(rel._cuts), 2) + if rel.has_convex_underestimator() or rel.has_concave_overestimator(): + cuts_len = len(rel._cuts) + else: + cuts_len = None + rel.rebuild() + if keep_cut: + if rel.has_convex_underestimator() or rel.has_concave_overestimator(): + self.assertEqual(cuts_len, len(rel._cuts)) + else: + self.assertIsNone(rel._cuts) + else: + if rel.has_convex_underestimator() or rel.has_concave_overestimator(): + self.assertEqual(len(rel._cuts), 2) + else: + self.assertIsNone(rel._cuts) + rel.clear_oa_points() + rel.rebuild() + if rel.has_convex_underestimator() or rel.has_concave_overestimator(): + self.assertEqual(len(rel._cuts), 2) + else: + self.assertIsNone(rel._cuts) + + def active_partition_helper(self, rel: coramin.relaxations.BasePWRelaxationData, partition_points): + rhs_var = rel.get_rhs_vars()[0] + sample_points = _grid_rhs_vars([rhs_var], 30) + partition_points.sort() + for pt in sample_points: + pt = pt[0] + rhs_var.value = pt + active_lb, active_ub = rel.get_active_partitions()[rhs_var] + assert partition_points[0] <= pt + assert partition_points[-1] >= pt + + ub_ndx = 0 + while partition_points[ub_ndx] < pt: + if ub_ndx == len(partition_points) - 1: + break + ub_ndx += 1 + if ub_ndx == 0: + ub_ndx = 1 + lb_ndx = ub_ndx - 1 + expected_lb = partition_points[lb_ndx] + expected_ub = partition_points[ub_ndx] + self.assertAlmostEqual(active_lb, expected_lb) + self.assertAlmostEqual(active_ub, expected_ub) + + def pw_helper(self, m: _BlockData, rel: coramin.relaxations.BasePWRelaxationData, rhs_expr: ExpressionBase): + rhs_vars = rel.get_rhs_vars() + sample_points = _grid_rhs_vars(rhs_vars, 5) + part_points = list(set(i[0] for i in sample_points)) + part_points.sort() + for pt in part_points: + rel.add_oa_point((pt,)) + rel.add_partition_point(pt) + rel.rebuild() + self.valid_relaxation_helper(m, rel, rhs_expr) + self.equal_at_points_helper(m, rel, rhs_expr, sample_points, True, True) + self.active_partition_helper(rel, part_points) + rel.clear_oa_points() + rel.clear_partitions() + rel.rebuild() + + def util_methods_helper(self, rel: coramin.relaxations.BaseRelaxationData, rhs_expr: ExpressionBase, + aux_var: _GeneralVarData, expected_convex: bool, expected_concave: bool, + supports_underestimator: bool = True, supports_overestimator: bool = True): + # test get_rhs_vars + expected = ComponentSet(identify_variables(rhs_expr)) + got = ComponentSet(rel.get_rhs_vars()) + diff = expected - got + self.assertEqual(len(diff), 0) + diff = got - expected + self.assertEqual(len(diff), 0) + self.assertEqual(type(rel.get_rhs_vars()), tuple) + + # test get_rhs_expr + expected = rhs_expr + got = rel.get_rhs_expr() + self.assertTrue(compare_expressions(expected, got)) + + # test get_aux_var + self.assertIs(rel.get_aux_var(), aux_var) + + # test convex/concave + self.assertEqual(rel.is_rhs_convex(), expected_convex) + self.assertEqual(rel.is_rhs_concave(), expected_concave) + + # test pprint + original_relaxation_side = rel.relaxation_side + if supports_underestimator and supports_overestimator: + out = io.StringIO() + rel.pprint(ostream=out) + self.assertIn(f'{str(rel.get_aux_var())} == {str(rhs_expr)}', out.getvalue()) + if supports_underestimator: + rel.relaxation_side = coramin.RelaxationSide.UNDER + rel.rebuild() + out = io.StringIO() + rel.pprint(ostream=out) + self.assertIn(f'{str(rel.get_aux_var())} >= {str(rhs_expr)}', out.getvalue()) + if supports_overestimator: + rel.relaxation_side = coramin.RelaxationSide.OVER + rel.rebuild() + out = io.StringIO() + rel.pprint(ostream=out) + self.assertIn(f'{str(rel.get_aux_var())} <= {str(rhs_expr)}', out.getvalue()) + rel.relaxation_side = original_relaxation_side + rel.rebuild() + rel.pprint(verbose=True) # only checks that an error does not get raised... + + def deviation_helper(self, rel: coramin.relaxations.BaseRelaxationData, rhs_expr: ExpressionBase, + supports_underestimator: bool = True, supports_overestimator: bool = True): + original_relaxation_side = rel.relaxation_side + for v in rel.get_rhs_vars(): + v.value = np.random.uniform(v.lb, v.ub) + rel.get_aux_var().value = pe.value(rhs_expr) + 1 + if supports_underestimator and supports_overestimator: + dev = rel.get_deviation() + self.assertAlmostEqual(dev, 1) + if supports_underestimator: + rel.relaxation_side = coramin.RelaxationSide.UNDER + dev = rel.get_deviation() + self.assertAlmostEqual(dev, 0) + if supports_overestimator: + rel.relaxation_side = coramin.RelaxationSide.OVER + dev = rel.get_deviation() + self.assertAlmostEqual(dev, 1) + rel.get_aux_var().value = pe.value(rhs_expr) - 1 + if supports_underestimator and supports_overestimator: + rel.relaxation_side = coramin.RelaxationSide.BOTH + dev = rel.get_deviation() + self.assertAlmostEqual(dev, 1) + if supports_underestimator: + rel.relaxation_side = coramin.RelaxationSide.UNDER + dev = rel.get_deviation() + self.assertAlmostEqual(dev, 1) + if supports_overestimator: + rel.relaxation_side = coramin.RelaxationSide.OVER + dev = rel.get_deviation() + self.assertAlmostEqual(dev, 0) + rel.relaxation_side = original_relaxation_side + + def small_coef_helper(self, m: _BlockData, rel: coramin.relaxations.BaseRelaxationData, rhs_expr: ExpressionBase, + num_points: int = 30, check_underestimator: bool = True, check_overestimator: bool = True): + rel.small_coef = 1e10 + rel.rebuild() + self.valid_relaxation_helper(m, rel, rhs_expr, num_points, check_underestimator, check_overestimator) + rel.small_coef = 1e-10 + rel.rebuild() + + def options_switching_helper(self, rel: coramin.relaxations.BaseRelaxationData): + self.assertIsNone(rel._original_constraint) + self.assertIsNone(rel._nonlinear) + self.assertIsNotNone(rel._oa_params) + self.assertIsNotNone(rel._cuts) + self.assertEqual(len(rel._cuts), 2) + rel.clear_oa_points() + self.assertEqual(len(rel._cuts), 0) + rel.add_oa_point(tuple(v.lb for v in rel.get_rhs_vars())) + self.assertEqual(len(rel._cuts), 0) + rel.rebuild(ensure_oa_at_vertices=False) + self.assertEqual(len(rel._cuts), 1) + rel.rebuild() + self.assertEqual(len(rel._cuts), 2) + rel.use_linear_relaxation = False + rel.rebuild() + self.assertIsNone(rel._original_constraint) + self.assertIsNone(rel._cuts) + self.assertIsNotNone(rel._nonlinear) + for v in rel.get_rhs_vars(): + v.value = 1 + with self.assertRaisesRegex(ValueError, 'Can only add an OA cut when using a linear relaxation'): + rel.add_cut(check_violation=False) + rel.rebuild(build_nonlinear_constraint=True) + self.assertIsNotNone(rel._original_constraint) + self.assertIsNone(rel._cuts) + self.assertIsNone(rel._nonlinear) + rel.use_linear_relaxation = True + rel.rebuild() + self.assertIsNone(rel._original_constraint) + self.assertIsNotNone(rel._cuts) + self.assertIsNone(rel._nonlinear) + + def get_base_pyomo_model(self, xlb=-1.5, xub=0.8, ylb=-2, yub=1): + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(xlb, xub)) + m.y = pe.Var(bounds=(ylb, yub)) + m.z = pe.Var() + return m + + def test_quadratic_relaxation(self): + m = self.get_base_pyomo_model() + m.rel = coramin.relaxations.PWXSquaredRelaxation() + m.rel.build(x=m.x, aux_var=m.z) + e = m.x**2 + self.options_switching_helper(m.rel) + self.valid_relaxation_helper(m, m.rel, e) + self.util_methods_helper(m.rel, e, m.z, True, False) + self.equal_at_points_helper(m, m.rel, e, [(-1.5,), (0.8,)]) + self.oa_cuts_helper(m, m.rel, e) + self.add_cuts_helper(m, m.rel, e) + self.pw_helper(m, m.rel, e) + self.changing_bounds_helper(m, m.rel, e) + self.infinite_bounds_helper(m, m.rel) + self.large_bounds_helper(m, m.rel) + self.small_coef_helper(m, m.rel, e) + self.original_constraint_helper(m, m.rel, e) + self.nonlinear_relaxation_helper(m, m.rel, e) + self.relaxation_side_helper(m, m.rel, e, check_nonlinear_relaxation=True) + self.deviation_helper(m.rel, e) + + def test_exp_relaxation(self): + m = self.get_base_pyomo_model() + m.rel = coramin.relaxations.PWUnivariateRelaxation() + e = pe.exp(m.x) + m.rel.build(x=m.x, aux_var=m.z, shape=coramin.utils.FunctionShape.CONVEX, f_x_expr=e) + self.options_switching_helper(m.rel) + self.valid_relaxation_helper(m, m.rel, e) + self.util_methods_helper(m.rel, e, m.z, True, False) + self.equal_at_points_helper(m, m.rel, e, [(-1.5,), (0.8,)]) + self.oa_cuts_helper(m, m.rel, e) + self.add_cuts_helper(m, m.rel, e) + self.pw_helper(m, m.rel, e) + self.changing_bounds_helper(m, m.rel, e) + self.infinite_bounds_helper(m, m.rel) + self.large_bounds_helper(m, m.rel) + self.small_coef_helper(m, m.rel, e) + self.original_constraint_helper(m, m.rel, e) + self.nonlinear_relaxation_helper(m, m.rel, e) + self.relaxation_side_helper(m, m.rel, e, check_nonlinear_relaxation=True) + self.deviation_helper(m.rel, e) + + def test_log_relaxation(self): + m = self.get_base_pyomo_model(xlb=0.1, xub=2.5) + m.rel = coramin.relaxations.PWUnivariateRelaxation() + e = pe.log(m.x) + m.rel.build(x=m.x, aux_var=m.z, shape=coramin.utils.FunctionShape.CONCAVE, f_x_expr=e) + self.options_switching_helper(m.rel) + self.valid_relaxation_helper(m, m.rel, e) + self.util_methods_helper(m.rel, e, m.z, False, True) + self.equal_at_points_helper(m, m.rel, e, [(0.1,), (2.5,)]) + self.oa_cuts_helper(m, m.rel, e) + self.add_cuts_helper(m, m.rel, e) + self.pw_helper(m, m.rel, e) + self.changing_bounds_helper(m, m.rel, e) + self.infinite_bounds_helper(m, m.rel) + m.rel.large_coef = 1e3 + self.large_bounds_helper(m, m.rel, lb=1e-4, ub=1e-3) + m.rel.large_coef = 1e5 + self.small_coef_helper(m, m.rel, e) + self.original_constraint_helper(m, m.rel, e) + self.nonlinear_relaxation_helper(m, m.rel, e) + self.relaxation_side_helper(m, m.rel, e, check_nonlinear_relaxation=True) + self.deviation_helper(m.rel, e) + + def test_univariate_convex_relaxation(self): + m = self.get_base_pyomo_model(xlb=0.1, xub=2.5) + m.rel = coramin.relaxations.PWUnivariateRelaxation() + e = m.x * pe.log(m.x) + m.rel.build(x=m.x, aux_var=m.z, shape=coramin.utils.FunctionShape.CONVEX, f_x_expr=e) + self.options_switching_helper(m.rel) + self.valid_relaxation_helper(m, m.rel, e) + self.util_methods_helper(m.rel, e, m.z, True, False) + self.equal_at_points_helper(m, m.rel, e, [(0.1,), (2.5,)]) + self.oa_cuts_helper(m, m.rel, e) + self.add_cuts_helper(m, m.rel, e) + self.pw_helper(m, m.rel, e) + self.changing_bounds_helper(m, m.rel, e) + self.infinite_bounds_helper(m, m.rel) + m.rel.large_coef = 0.1 + self.large_bounds_helper(m, m.rel) + m.rel.large_coef = 1e5 + self.small_coef_helper(m, m.rel, e) + self.original_constraint_helper(m, m.rel, e) + self.nonlinear_relaxation_helper(m, m.rel, e) + self.relaxation_side_helper(m, m.rel, e, check_nonlinear_relaxation=True) + self.deviation_helper(m.rel, e) + + def test_cos_relaxation(self): + m = self.get_base_pyomo_model() + m.rel = coramin.relaxations.PWCosRelaxation() + m.rel.build(x=m.x, aux_var=m.z) + e = pe.cos(m.x) + self.options_switching_helper(m.rel) + self.valid_relaxation_helper(m, m.rel, e) + self.util_methods_helper(m.rel, e, m.z, False, True) + self.equal_at_points_helper(m, m.rel, e, [(-1.5,), (0.8,)]) + self.oa_cuts_helper(m, m.rel, e) + self.add_cuts_helper(m, m.rel, e) + self.pw_helper(m, m.rel, e) + self.changing_bounds_helper(m, m.rel, e) + self.infinite_bounds_helper(m, m.rel) + self.large_bounds_helper(m, m.rel) + self.small_coef_helper(m, m.rel, e) + self.original_constraint_helper(m, m.rel, e) + self.nonlinear_relaxation_helper(m, m.rel, e) + self.relaxation_side_helper(m, m.rel, e, check_nonlinear_relaxation=True) + self.deviation_helper(m.rel, e) + + def test_sin_relaxation(self): + m = self.get_base_pyomo_model() + m.rel = coramin.relaxations.PWSinRelaxation() + m.rel.build(x=m.x, aux_var=m.z) + e = pe.sin(m.x) + self.valid_relaxation_helper(m, m.rel, e) + self.util_methods_helper(m.rel, e, m.z, False, False) + self.equal_at_points_helper(m, m.rel, e, [(-1.5,), (0.8,)]) + self.oa_cuts_helper(m, m.rel, e) + self.add_cuts_helper(m, m.rel, e) + self.pw_helper(m, m.rel, e) + self.changing_bounds_helper(m, m.rel, e) + self.infinite_bounds_helper(m, m.rel) + self.large_bounds_helper(m, m.rel) + self.small_coef_helper(m, m.rel, e) + self.original_constraint_helper(m, m.rel, e) + self.nonlinear_relaxation_helper(m, m.rel, e) + self.relaxation_side_helper(m, m.rel, e, check_nonlinear_relaxation=True) + self.deviation_helper(m.rel, e) + + def test_atan_relaxation(self): + m = self.get_base_pyomo_model() + m.rel = coramin.relaxations.PWArctanRelaxation() + m.rel.build(x=m.x, aux_var=m.z) + e = pe.atan(m.x) + self.valid_relaxation_helper(m, m.rel, e) + self.util_methods_helper(m.rel, e, m.z, False, False) + self.equal_at_points_helper(m, m.rel, e, [(-1.5,), (0.8,)]) + self.oa_cuts_helper(m, m.rel, e) + self.add_cuts_helper(m, m.rel, e) + self.pw_helper(m, m.rel, e) + self.changing_bounds_helper(m, m.rel, e) + self.infinite_bounds_helper(m, m.rel) + m.rel.large_coef = 0.1 + self.large_bounds_helper(m, m.rel) + m.rel.large_coef = 1e5 + self.small_coef_helper(m, m.rel, e) + self.original_constraint_helper(m, m.rel, e) + self.nonlinear_relaxation_helper(m, m.rel, e) + self.relaxation_side_helper(m, m.rel, e, check_nonlinear_relaxation=True) + self.deviation_helper(m.rel, e) + + def test_bilinear_relaxation(self): + m = self.get_base_pyomo_model() + m.rel = coramin.relaxations.PWMcCormickRelaxation() + m.rel.build(x1=m.x, x2=m.y, aux_var=m.z) + e = m.x * m.y + self.valid_relaxation_helper(m, m.rel, e) + self.util_methods_helper(m.rel, e, m.z, False, False) + self.equal_at_points_helper(m, m.rel, e, [(-1.5, -2), (0.8, 1), (-1.5, 1), (0.8, -2)]) + self.oa_cuts_helper(m, m.rel, e) + self.add_cuts_helper(m, m.rel, e) + self.pw_helper(m, m.rel, e) + self.changing_bounds_helper(m, m.rel, e, num_points=5) + self.infinite_bounds_helper(m, m.rel) + self.large_bounds_helper(m, m.rel, lb=-1e6, ub=1e6) + self.small_coef_helper(m, m.rel, e) + self.original_constraint_helper(m, m.rel, e) + with self.assertRaisesRegex(ValueError, "Relaxations of type do not support relaxations that are not linear."): + self.nonlinear_relaxation_helper(m, m.rel, e) + self.relaxation_side_helper(m, m.rel, e, check_nonlinear_relaxation=False) + self.deviation_helper(m.rel, e) + + def test_multivariate_convex(self): + m = self.get_base_pyomo_model() + m.rel = coramin.relaxations.MultivariateRelaxation() + m.rel.build(aux_var=m.z, shape=coramin.FunctionShape.CONVEX, f_x_expr=m.x**2 + m.y**2) + e = m.x**2 + m.y**2 + self.options_switching_helper(m.rel) + self.valid_relaxation_helper(m, m.rel, e, 10, True, False) + self.util_methods_helper(m.rel, e, m.z, True, False, True, False) + with self.assertRaises(ValueError): + m.rel.relaxation_side = coramin.RelaxationSide.OVER + with self.assertRaises(ValueError): + m.rel.relaxation_side = coramin.RelaxationSide.BOTH + self.equal_at_points_helper(m, m.rel, e, [(-1.5, -2), (0.8, 1)], True, False, True) + self.oa_cuts_helper(m, m.rel, e, 30, True, False) + self.add_cuts_helper(m, m.rel, e, 30, True, False) + self.changing_bounds_helper(m, m.rel, e, 5, True, False) + self.infinite_bounds_helper(m, m.rel) + self.large_bounds_helper(m, m.rel) + self.small_coef_helper(m, m.rel, e, 30, True, False) + self.original_constraint_helper(m, m.rel, e, 15, True, False) + self.nonlinear_relaxation_helper(m, m.rel, e, 15, True, False) + self.deviation_helper(m.rel, e, True, False) + + def test_multivariate_concave(self): + m = self.get_base_pyomo_model() + m.rel = coramin.relaxations.MultivariateRelaxation() + m.rel.build(aux_var=m.z, shape=coramin.FunctionShape.CONCAVE, f_x_expr=-m.x**2 - m.y**2) + e = -m.x**2 - m.y**2 + self.valid_relaxation_helper(m, m.rel, e, 10, False, True) + self.util_methods_helper(m.rel, e, m.z, False, True, False, True) + with self.assertRaises(ValueError): + m.rel.relaxation_side = coramin.RelaxationSide.UNDER + with self.assertRaises(ValueError): + m.rel.relaxation_side = coramin.RelaxationSide.BOTH + self.equal_at_points_helper(m, m.rel, e, [(-1.5, -2), (0.8, 1)], False, True, True) + self.oa_cuts_helper(m, m.rel, e, 30, False, True) + self.add_cuts_helper(m, m.rel, e, 30, False, True) + self.changing_bounds_helper(m, m.rel, e, 5, False, True) + self.infinite_bounds_helper(m, m.rel) + self.large_bounds_helper(m, m.rel) + self.small_coef_helper(m, m.rel, e, 30, False, True) + self.original_constraint_helper(m, m.rel, e, 15, False, True) + self.nonlinear_relaxation_helper(m, m.rel, e, 15, False, True) + self.deviation_helper(m.rel, e, False, True) + + def test_alpha_bb1(self): + m = self.get_base_pyomo_model() + m.rel = coramin.relaxations.AlphaBBRelaxation() + m.rel.build( + aux_var=m.z, f_x_expr=m.x*m.y, relaxation_side=coramin.RelaxationSide.UNDER, + eigenvalue_opt=appsi.solvers.Gurobi(), + ) + e = m.x*m.y + self.options_switching_helper(m.rel) + self.valid_relaxation_helper(m, m.rel, e, 10, True, False) + self.util_methods_helper(m.rel, e, m.z, False, False, True, False) + with self.assertRaises(ValueError): + m.rel.relaxation_side = coramin.RelaxationSide.OVER + with self.assertRaises(ValueError): + m.rel.relaxation_side = coramin.RelaxationSide.BOTH + self.equal_at_points_helper(m, m.rel, e, [(-1.5, -2), (0.8, 1)], True, False, True) + self.oa_cuts_helper(m, m.rel, e, 30, True, False, False) + self.add_cuts_helper(m, m.rel, e, 30, True, False, False) + self.infinite_bounds_helper(m, m.rel) + self.large_bounds_helper(m, m.rel) + self.small_coef_helper(m, m.rel, e, 30, True, False) + self.original_constraint_helper(m, m.rel, e, 15, True, False) + self.deviation_helper(m.rel, e, True, False) + + def test_alpha_bb2(self): + m = self.get_base_pyomo_model() + m.rel = coramin.relaxations.AlphaBBRelaxation() + m.rel.build( + aux_var=m.z, f_x_expr=-m.x**2 - m.y**2, + relaxation_side=coramin.RelaxationSide.UNDER, + eigenvalue_opt=appsi.solvers.Gurobi(), + ) + e = -m.x**2 - m.y**2 + self.valid_relaxation_helper(m, m.rel, e, 10, True, False) + self.util_methods_helper(m.rel, e, m.z, False, True, True, False) + with self.assertRaises(ValueError): + m.rel.relaxation_side = coramin.RelaxationSide.OVER + with self.assertRaises(ValueError): + m.rel.relaxation_side = coramin.RelaxationSide.BOTH + self.equal_at_points_helper(m, m.rel, e, [(-1.5, -2), (0.8, 1)], True, False, True) + self.oa_cuts_helper(m, m.rel, e, 30, True, False, False) + self.add_cuts_helper(m, m.rel, e, 30, True, False, False) + self.changing_bounds_helper(m, m.rel, e, 5, True, False, False) + self.infinite_bounds_helper(m, m.rel) + self.large_bounds_helper(m, m.rel) + self.small_coef_helper(m, m.rel, e, 30, True, False) + self.original_constraint_helper(m, m.rel, e, 15, True, False) + self.nonlinear_relaxation_helper(m, m.rel, e, 15, True, False, False) + self.deviation_helper(m.rel, e, True, False) + + def test_alpha_bb3(self): + m = self.get_base_pyomo_model() + m.rel = coramin.relaxations.AlphaBBRelaxation() + m.rel.build( + aux_var=m.z, f_x_expr=m.x**2 + m.y**2, + relaxation_side=coramin.RelaxationSide.UNDER, + eigenvalue_opt=appsi.solvers.Gurobi(), + ) + e = m.x**2 + m.y**2 + self.valid_relaxation_helper(m, m.rel, e, 10, True, False) + self.util_methods_helper(m.rel, e, m.z, True, False, True, False) + with self.assertRaises(ValueError): + m.rel.relaxation_side = coramin.RelaxationSide.OVER + with self.assertRaises(ValueError): + m.rel.relaxation_side = coramin.RelaxationSide.BOTH + self.equal_at_points_helper(m, m.rel, e, [(-1.5, -2), (0.8, 1)], True, False, True) + self.oa_cuts_helper(m, m.rel, e, 30, True, False, True) + self.add_cuts_helper(m, m.rel, e, 30, True, False, True) + self.changing_bounds_helper(m, m.rel, e, 5, True, False, True) + self.infinite_bounds_helper(m, m.rel) + self.large_bounds_helper(m, m.rel) + self.small_coef_helper(m, m.rel, e, 30, True, False) + self.original_constraint_helper(m, m.rel, e, 15, True, False) + self.nonlinear_relaxation_helper(m, m.rel, e, 15, True, False, True) + self.deviation_helper(m.rel, e, True, False) diff --git a/pyomo/contrib/coramin/relaxations/tests/test_relaxations_base.py b/pyomo/contrib/coramin/relaxations/tests/test_relaxations_base.py new file mode 100644 index 00000000000..186b5128f33 --- /dev/null +++ b/pyomo/contrib/coramin/relaxations/tests/test_relaxations_base.py @@ -0,0 +1,161 @@ +import unittest +import pyomo.environ as pe +from pyomo.opt import assert_optimal_termination +import coramin +from pyomo.core.base.var import SimpleVar + + +""" +Things to test +- relaxations are valid under all possible conditions + - continuous relaxations + - nonlinear relaxations + - pw relaxations +- infinite variable bounds +- tightness of relaxations + - as variable bounds improve, the relaxations should get tighter + - at variable bounds, relaxations should be exact + - cuts improve relaxation +- large coef +- small coef +- safety tol +- relaxation side +- modifications to relaxations +- rebuild + - if things don't need removed, don't remove them + - rebuild with original constraint +- cloning +""" + + +class TestBaseRelaxation(unittest.TestCase): + def test_push_and_pop_oa_points(self): + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(-2, 1)) + m.y = pe.Var() + m.rel = coramin.relaxations.PWXSquaredRelaxation() + m.rel.build(x=m.x, aux_var=m.y) + m.obj = pe.Objective(expr=m.y) + + opt = pe.SolverFactory('appsi_gurobi') + + res = opt.solve(m) + assert_optimal_termination(res) + self.assertAlmostEqual(m.x.value, -0.5) + self.assertAlmostEqual(m.y.value, -2) + + m.x.value = -1 + m.rel.add_cut(keep_cut=True) + res = opt.solve(m) + assert_optimal_termination(res) + self.assertAlmostEqual(m.x.value, 0) + self.assertAlmostEqual(m.y.value, -1) + + m.rel.push_oa_points() + m.rel.rebuild() + res = opt.solve(m) + assert_optimal_termination(res) + self.assertAlmostEqual(m.x.value, 0) + self.assertAlmostEqual(m.y.value, -1) + + m.rel.clear_oa_points() + m.rel.rebuild() + res = opt.solve(m) + assert_optimal_termination(res) + self.assertAlmostEqual(m.x.value, -0.5) + self.assertAlmostEqual(m.y.value, -2) + + m.x.value = -0.5 + m.rel.add_cut(keep_cut=True) + res = opt.solve(m) + assert_optimal_termination(res) + self.assertAlmostEqual(m.x.value, 0.25) + self.assertAlmostEqual(m.y.value, -0.5) + + m.rel.pop_oa_points() + m.rel.rebuild() + res = opt.solve(m) + assert_optimal_termination(res) + self.assertAlmostEqual(m.x.value, 0) + self.assertAlmostEqual(m.y.value, -1) + + def test_push_oa_points_with_key(self): + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(-1, 1)) + m.y = pe.Var() + m.c = coramin.relaxations.PWXSquaredRelaxation() + m.c.build(x=m.x, aux_var=m.y) + m.c.add_oa_point(pe.ComponentMap([(m.x, 0)])) + self.assertEqual(list(m.c._oa_points.keys()), [(-1,), (1,), (0,)]) + m.c.push_oa_points(key='first key') + m.c.add_oa_point(pe.ComponentMap([(m.x, 0.5)])) + self.assertEqual(list(m.c._oa_points.keys()), [(-1,), (1,), (0,), (0.5,)]) + m.c.push_oa_points() + m.c.add_oa_point(pe.ComponentMap([(m.x, -0.5)])) + self.assertEqual(list(m.c._oa_points.keys()), [(-1,), (1,), (0,), (0.5,), (-0.5,)]) + m.c.push_oa_points(key='second key') + m.c.pop_oa_points(key='first key') + self.assertEqual(list(m.c._oa_points.keys()), [(-1,), (1,), (0,)]) + m.c.pop_oa_points() + self.assertEqual(list(m.c._oa_points.keys()), [(-1,), (1,), (0,), (0.5,)]) + m.c.pop_oa_points(key='second key') + self.assertEqual(list(m.c._oa_points.keys()), [(-1,), (1,), (0,), (0.5,), (-0.5,)]) + + def test_push_and_pop_partitions(self): + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(-2, 1)) + m.y = pe.Var() + m.rel = coramin.relaxations.PWXSquaredRelaxation() + m.rel.build(x=m.x, aux_var=m.y) + m.obj = pe.Objective(expr=m.y) + self.assertEqual(m.rel._partitions[m.x], [-2, 1]) + + m.rel.add_partition_point(-1) + m.rel.rebuild() + self.assertEqual(m.rel._partitions[m.x], [-2, -1, 1]) + + m.rel.push_partitions() + m.rel.rebuild() + self.assertEqual(m.rel._partitions[m.x], [-2, -1, 1]) + + m.rel.clear_partitions() + m.rel.rebuild() + self.assertEqual(m.rel._partitions[m.x], [-2, 1]) + + m.rel.add_partition_point(-0.5) + m.rel.rebuild() + self.assertEqual(m.rel._partitions[m.x], [-2, -0.5, 1]) + + m.rel.pop_partitions() + m.rel.rebuild() + self.assertEqual(m.rel._partitions[m.x], [-2, -1, 1]) + + def test_push_and_pop_partitions_2(self): + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(-1, 1)) + m.y = pe.Var() + m.c = coramin.relaxations.PWXSquaredRelaxation() + m.c.build(x=m.x, aux_var=m.y) + self.assertEqual(m.c._partitions, pe.ComponentMap([(m.x, [-1, 1])])) + m.x.setlb(0) + m.c.rebuild() + self.assertEqual(m.c._partitions, pe.ComponentMap([(m.x, [0, 1])])) + m.x.setlb(-1) + m.c.rebuild() + self.assertEqual(m.c._partitions, pe.ComponentMap([(m.x, [-1, 1])])) + m.x.value = 0.5 + m.c.add_partition_point() + m.c.rebuild() + self.assertEqual(m.c._partitions, pe.ComponentMap([(m.x, [-1, 0.5, 1])])) + m.x.setlb(0) + m.c.rebuild() + self.assertEqual(m.c._partitions, pe.ComponentMap([(m.x, [0, 0.5, 1])])) + m.x.setlb(-1) + m.c.rebuild() + self.assertEqual(m.c._partitions, pe.ComponentMap([(m.x, [-1, 0.5, 1])])) + m.x.setub(0) + m.c.rebuild() + self.assertEqual(m.c._partitions, pe.ComponentMap([(m.x, [-1, 0])])) + m.x.setub(1) + m.c.rebuild() + self.assertEqual(m.c._partitions, pe.ComponentMap([(m.x, [-1, 1])])) diff --git a/pyomo/contrib/coramin/relaxations/tests/test_univariate_relaxations.py b/pyomo/contrib/coramin/relaxations/tests/test_univariate_relaxations.py new file mode 100644 index 00000000000..a7c2053534a --- /dev/null +++ b/pyomo/contrib/coramin/relaxations/tests/test_univariate_relaxations.py @@ -0,0 +1,274 @@ +import unittest +import math +import pyomo.environ as pe +import coramin +import numpy as np +from coramin.relaxations.segments import compute_k_segment_points + + +class TestUnivariateExp(unittest.TestCase): + @classmethod + def setUpClass(cls): + model = pe.ConcreteModel() + cls.model = model + model.y = pe.Var() + model.x = pe.Var(bounds=(-1.5, 1.5)) + + model.obj = pe.Objective(expr=model.y, sense=pe.maximize) + model.pw_exp = coramin.relaxations.PWUnivariateRelaxation() + model.pw_exp.build(x=model.x, aux_var=model.y, pw_repn='INC', shape=coramin.utils.FunctionShape.CONVEX, + relaxation_side=coramin.utils.RelaxationSide.BOTH, f_x_expr=pe.exp(model.x)) + model.pw_exp.add_partition_point(-0.5) + model.pw_exp.add_partition_point(0.5) + model.pw_exp.rebuild() + + @classmethod + def tearDownClass(cls): + pass + + def test_exp_ub(self): + model = self.model.clone() + + solver = pe.SolverFactory('gurobi_direct') + solver.solve(model) + self.assertAlmostEqual(pe.value(model.y), math.exp(1.5), 4) + + def test_exp_mid(self): + model = self.model.clone() + model.x_con = pe.Constraint(expr=model.x <= 0.3) + + solver = pe.SolverFactory('gurobi_direct') + solver.solve(model) + self.assertAlmostEqual(pe.value(model.y), 1.44, 3) + + def test_exp_lb(self): + model = self.model.clone() + model.obj.sense = pe.minimize + + solver = pe.SolverFactory('gurobi_direct') + solver.solve(model) + self.assertAlmostEqual(pe.value(model.y), math.exp(-1.5), 4) + + +class TestUnivariate(unittest.TestCase): + def helper(self, func, shape, bounds_list, relaxation_class, relaxation_side=coramin.utils.RelaxationSide.BOTH): + for lb, ub in bounds_list: + num_segments_list = [1, 2, 3] + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(lb, ub)) + m.aux = pe.Var() + if relaxation_class is coramin.relaxations.PWUnivariateRelaxation: + m.c = coramin.relaxations.PWUnivariateRelaxation() + m.c.build(x=m.x, + aux_var=m.aux, + relaxation_side=relaxation_side, + shape=shape, + f_x_expr=func(m.x)) + else: + m.c = relaxation_class() + m.c.build(x=m.x, aux_var=m.aux, relaxation_side=relaxation_side) + m.p = pe.Param(mutable=True, initialize=0) + m.c2 = pe.Constraint(expr=m.x == m.p) + opt = pe.SolverFactory('gurobi_persistent') + for num_segments in num_segments_list: + segment_points = compute_k_segment_points(m.x, num_segments) + m.c.clear_partitions() + for pt in segment_points: + m.c.add_partition_point(pt) + var_values = pe.ComponentMap() + var_values[m.x] = pt + m.c.add_oa_point(var_values=var_values) + m.c.rebuild() + opt.set_instance(m) + for _x in [float(i) for i in np.linspace(lb, ub, 10)]: + m.p.value = _x + opt.remove_constraint(m.c2) + opt.add_constraint(m.c2) + if relaxation_side in {coramin.utils.RelaxationSide.BOTH, coramin.utils.RelaxationSide.UNDER}: + m.obj = pe.Objective(expr=m.aux) + opt.set_objective(m.obj) + res = opt.solve() + self.assertEqual(res.solver.termination_condition, pe.TerminationCondition.optimal) + self.assertLessEqual(m.aux.value, func(_x) + 1e-10) + del m.obj + if relaxation_side in {coramin.utils.RelaxationSide.BOTH, coramin.utils.RelaxationSide.OVER}: + m.obj = pe.Objective(expr=m.aux, sense=pe.maximize) + opt.set_objective(m.obj) + res = opt.solve() + self.assertEqual(res.solver.termination_condition, pe.TerminationCondition.optimal) + self.assertGreaterEqual(m.aux.value, func(_x) - 1e-10) + del m.obj + + def test_exp(self): + self.helper(func=pe.exp, shape=coramin.utils.FunctionShape.CONVEX, bounds_list=[(-1, 1)], + relaxation_class=coramin.relaxations.PWUnivariateRelaxation) + + def test_log(self): + self.helper(func=pe.log, shape=coramin.utils.FunctionShape.CONCAVE, bounds_list=[(0.5, 1.5)], + relaxation_class=coramin.relaxations.PWUnivariateRelaxation) + + def test_quadratic(self): + def quadratic_func(x): + return x**2 + self.helper(func=quadratic_func, shape=None, bounds_list=[(-1, 2)], + relaxation_class=coramin.relaxations.PWXSquaredRelaxation) + + def test_arctan(self): + self.helper(func=pe.atan, shape=None, bounds_list=[(-1, 1), (-1, 0), (0, 1)], + relaxation_class=coramin.relaxations.PWArctanRelaxation) + self.helper(func=pe.atan, shape=None, bounds_list=[(-0.1, 1)], + relaxation_class=coramin.relaxations.PWArctanRelaxation, + relaxation_side=coramin.utils.RelaxationSide.OVER) + self.helper(func=pe.atan, shape=None, bounds_list=[(-1, 0.1)], + relaxation_class=coramin.relaxations.PWArctanRelaxation, + relaxation_side=coramin.utils.RelaxationSide.UNDER) + + def test_sin(self): + self.helper(func=pe.sin, shape=None, bounds_list=[(-1, 1), (-1, 0), (0, 1)], + relaxation_class=coramin.relaxations.PWSinRelaxation) + self.helper(func=pe.sin, shape=None, bounds_list=[(-0.1, 1)], + relaxation_class=coramin.relaxations.PWSinRelaxation, + relaxation_side=coramin.utils.RelaxationSide.OVER) + self.helper(func=pe.sin, shape=None, bounds_list=[(-1, 0.1)], + relaxation_class=coramin.relaxations.PWSinRelaxation, + relaxation_side=coramin.utils.RelaxationSide.UNDER) + + def test_cos(self): + self.helper(func=pe.cos, shape=None, bounds_list=[(-1, 1)], + relaxation_class=coramin.relaxations.PWCosRelaxation) + + +class TestFeasibility(unittest.TestCase): + def test_univariate_exp(self): + m = pe.ConcreteModel() + m.p = pe.Param(initialize=-1, mutable=True) + m.x = pe.Var(bounds=(-1, 1)) + m.y = pe.Var() + m.z = pe.Var(bounds=(0, None)) + m.c = coramin.relaxations.PWUnivariateRelaxation() + m.c.build(x=m.x, aux_var=m.y, relaxation_side=coramin.utils.RelaxationSide.BOTH, + shape=coramin.utils.FunctionShape.CONVEX, f_x_expr=pe.exp(m.x)) + m.c.rebuild() + m.c2 = pe.ConstraintList() + m.c2.add(m.z >= m.y - m.p) + m.c2.add(m.z >= m.p - m.y) + m.obj = pe.Objective(expr=m.z) + opt = pe.SolverFactory('gurobi_direct') + for xval in [-1, -0.5, 0, 0.5, 1]: + pval = math.exp(xval) + m.x.fix(xval) + m.p.value = pval + res = opt.solve(m, tee=False) + self.assertTrue(res.solver.termination_condition == pe.TerminationCondition.optimal) + self.assertAlmostEqual(m.y.value, m.p.value, 6) + + def test_pw_exp(self): + m = pe.ConcreteModel() + m.p = pe.Param(initialize=-1, mutable=True) + m.x = pe.Var(bounds=(-1, 1)) + m.y = pe.Var() + m.z = pe.Var(bounds=(0, None)) + m.c = coramin.relaxations.PWUnivariateRelaxation() + m.c.build(x=m.x, aux_var=m.y, relaxation_side=coramin.utils.RelaxationSide.BOTH, + shape=coramin.utils.FunctionShape.CONVEX, f_x_expr=pe.exp(m.x)) + m.c.add_partition_point(-0.25) + m.c.add_partition_point(0.25) + m.c.rebuild() + m.c2 = pe.ConstraintList() + m.c2.add(m.z >= m.y - m.p) + m.c2.add(m.z >= m.p - m.y) + m.obj = pe.Objective(expr=m.z) + opt = pe.SolverFactory('gurobi_direct') + for xval in [-1, -0.5, 0, 0.5, 1]: + pval = math.exp(xval) + m.x.fix(xval) + m.p.value = pval + res = opt.solve(m, tee=False) + self.assertTrue(res.solver.termination_condition == pe.TerminationCondition.optimal) + self.assertAlmostEqual(m.y.value, m.p.value, 6) + + def test_univariate_log(self): + m = pe.ConcreteModel() + m.p = pe.Param(initialize=-1, mutable=True) + m.x = pe.Var(bounds=(0.5, 1.5)) + m.y = pe.Var() + m.z = pe.Var(bounds=(0, None)) + m.c = coramin.relaxations.PWUnivariateRelaxation() + m.c.build(x=m.x, aux_var=m.y, relaxation_side=coramin.utils.RelaxationSide.BOTH, + shape=coramin.utils.FunctionShape.CONCAVE, f_x_expr=pe.log(m.x)) + m.c.rebuild() + m.c2 = pe.ConstraintList() + m.c2.add(m.z >= m.y - m.p) + m.c2.add(m.z >= m.p - m.y) + m.obj = pe.Objective(expr=m.z) + opt = pe.SolverFactory('gurobi_direct') + for xval in [0.5, 0.75, 1, 1.25, 1.5]: + pval = math.log(xval) + m.x.fix(xval) + m.p.value = pval + res = opt.solve(m, tee=False) + self.assertTrue(res.solver.termination_condition == pe.TerminationCondition.optimal) + self.assertAlmostEqual(m.y.value, m.p.value, 6) + + def test_pw_log(self): + m = pe.ConcreteModel() + m.p = pe.Param(initialize=-1, mutable=True) + m.x = pe.Var(bounds=(0.5, 1.5)) + m.y = pe.Var() + m.z = pe.Var(bounds=(0, None)) + m.c = coramin.relaxations.PWUnivariateRelaxation() + m.c.build(x=m.x, aux_var=m.y, relaxation_side=coramin.utils.RelaxationSide.BOTH, + shape=coramin.utils.FunctionShape.CONCAVE, f_x_expr=pe.log(m.x)) + m.c.add_partition_point(0.9) + m.c.add_partition_point(1.1) + m.c.rebuild() + m.c2 = pe.ConstraintList() + m.c2.add(m.z >= m.y - m.p) + m.c2.add(m.z >= m.p - m.y) + m.obj = pe.Objective(expr=m.z) + opt = pe.SolverFactory('gurobi_direct') + for xval in [0.5, 0.75, 1, 1.25, 1.5]: + pval = math.log(xval) + m.x.fix(xval) + m.p.value = pval + res = opt.solve(m, tee=False) + self.assertTrue(res.solver.termination_condition == pe.TerminationCondition.optimal) + self.assertAlmostEqual(m.y.value, m.p.value, 6) + + def test_x_fixed(self): + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(-1, 1)) + m.y = pe.Var() + m.x.setlb(0) + m.x.setub(0) + m.c = coramin.relaxations.PWUnivariateRelaxation() + m.c.build(x=m.x, aux_var=m.y, relaxation_side=coramin.utils.RelaxationSide.BOTH, + shape=coramin.utils.FunctionShape.CONVEX, f_x_expr=pe.exp(m.x)) + m.obj = pe.Objective(expr=m.y) + opt = pe.SolverFactory('appsi_gurobi') + res = opt.solve(m) + self.assertAlmostEqual(m.y.value, 1) + m.obj.sense = pe.maximize + res = opt.solve(m) + self.assertAlmostEqual(m.y.value, 1) + + def test_x_sq(self): + m = pe.ConcreteModel() + m.p = pe.Param(initialize=-1, mutable=True) + m.x = pe.Var(bounds=(-1, 1)) + m.y = pe.Var() + m.z = pe.Var(bounds=(0, None)) + m.c = coramin.relaxations.PWXSquaredRelaxation() + m.c.build(x=m.x, aux_var=m.y, relaxation_side=coramin.utils.RelaxationSide.BOTH) + m.c2 = pe.ConstraintList() + m.c2.add(m.z >= m.y - m.p) + m.c2.add(m.z >= m.p - m.y) + m.obj = pe.Objective(expr=m.z) + opt = pe.SolverFactory('appsi_gurobi') + for xval in [-1, -0.5, 0, 0.5, 1]: + pval = xval**2 + m.x.fix(xval) + m.p.value = pval + res = opt.solve(m, tee=False) + self.assertTrue(res.solver.termination_condition == pe.TerminationCondition.optimal) + self.assertAlmostEqual(m.y.value, m.p.value, 6) diff --git a/pyomo/contrib/coramin/relaxations/univariate.py b/pyomo/contrib/coramin/relaxations/univariate.py new file mode 100644 index 00000000000..0025cf1490c --- /dev/null +++ b/pyomo/contrib/coramin/relaxations/univariate.py @@ -0,0 +1,1060 @@ +import pyomo.environ as pyo +from coramin.utils.coramin_enums import RelaxationSide, FunctionShape +from .relaxations_base import BasePWRelaxationData, ComponentWeakRef, _check_cut +from .custom_block import declare_custom_block +import numpy as np +import math +import scipy.optimize +from ._utils import check_var_pts, _get_bnds_list, _get_bnds_tuple +from pyomo.core.base.param import ScalarParam, IndexedParam +from pyomo.core.base.constraint import ScalarConstraint, IndexedConstraint +from pyomo.core.expr.numeric_expr import LinearExpression +import logging +from typing import Optional, Union, Sequence +from pyomo.core.expr.calculus.diff_with_pyomo import reverse_sd +logger = logging.getLogger(__name__) +pe = pyo + + +def _sin_overestimator_fn(x, LB): + return np.sin(x) + np.cos(x) * (LB - x) - np.sin(LB) + + +def _sin_underestimator_fn(x, UB): + return np.sin(x) + np.cos(-x) * (UB - x) - np.sin(UB) + + +def _compute_sine_overestimator_tangent_point(vlb): + assert vlb < 0 + tangent_point, res = scipy.optimize.bisect(f=_sin_overestimator_fn, a=0, b=math.pi / 2, args=(vlb,), + full_output=True, disp=False) + if res.converged: + tangent_point = float(tangent_point) + slope = float(np.cos(tangent_point)) + intercept = float(np.sin(vlb) - slope * vlb) + return tangent_point, slope, intercept + else: + raise RuntimeError('Unable to build relaxation for sin(x)\nBisect info: ' + str(res)) + + +def _compute_sine_underestimator_tangent_point(vub): + assert vub > 0 + tangent_point, res = scipy.optimize.bisect(f=_sin_underestimator_fn, a=-math.pi / 2, b=0, args=(vub,), + full_output=True, disp=False) + if res.converged: + tangent_point = float(tangent_point) + slope = float(np.cos(-tangent_point)) + intercept = float(np.sin(vub) - slope * vub) + return tangent_point, slope, intercept + else: + raise RuntimeError('Unable to build relaxation for sin(x)\nBisect info: ' + str(res)) + + +def _atan_overestimator_fn(x, LB): + return (1 + x**2) * (np.arctan(x) - np.arctan(LB)) - x + LB + + +def _atan_underestimator_fn(x, UB): + return (1 + x**2) * (np.arctan(x) - np.arctan(UB)) - x + UB + + +def _compute_arctan_overestimator_tangent_point(vlb): + assert vlb < 0 + tangent_point, res = scipy.optimize.bisect(f=_atan_overestimator_fn, a=0, b=abs(vlb), args=(vlb,), + full_output=True, disp=False) + if res.converged: + tangent_point = float(tangent_point) + slope = 1/(1 + tangent_point**2) + intercept = float(np.arctan(vlb) - slope * vlb) + return tangent_point, slope, intercept + else: + raise RuntimeError('Unable to build relaxation for arctan(x)\nBisect info: ' + str(res)) + + +def _compute_arctan_underestimator_tangent_point(vub): + assert vub > 0 + tangent_point, res = scipy.optimize.bisect(f=_atan_underestimator_fn, a=-vub, b=0, args=(vub,), + full_output=True, disp=False) + if res.converged: + tangent_point = float(tangent_point) + slope = 1/(1 + tangent_point**2) + intercept = float(np.arctan(vub) - slope * vub) + return tangent_point, slope, intercept + else: + raise RuntimeError('Unable to build relaxation for arctan(x)\nBisect info: ' + str(res)) + + +class _FxExpr(object): + def __init__(self, expr, x): + self._expr = expr + self._x = x + self._deriv = reverse_sd(expr)[x] + + def eval(self, _xval): + _xval = pyo.value(_xval) + orig_xval = self._x.value + self._x.value = _xval + res = pyo.value(self._expr) + self._x.set_value(orig_xval, skip_validation=True) + return res + + def deriv(self, _xval): + _xval = pyo.value(_xval) + orig_xval = self._x.value + self._x.value = _xval + res = pyo.value(self._deriv) + self._x.set_value(orig_xval, skip_validation=True) + return res + + def __call__(self, _xval): + return self.eval(_xval) + + +def _func_wrapper(obj): + def _func(m, val): + return obj(val) + return _func + + +def _pw_univariate_relaxation(b, x, w, x_pts, f_x_expr, pw_repn='INC', shape=FunctionShape.UNKNOWN, + relaxation_side=RelaxationSide.BOTH, large_eval_tol=math.inf, + safety_tol=0): + """ + This function creates piecewise envelopes to relax "w=f(x)" where f(x) is univariate and either convex over the + entire domain of x or concave over the entire domain of x. + + Parameters + ---------- + b: pyo.Block + x: pyo.Var + The "x" variable in f(x) + w: pyo.Var + The "w" variable that is replacing f(x) + x_pts: Sequence[float] + A list of floating point numbers to define the points over which the piecewise representation will generated. + This list must be ordered, and it is expected that the first point (x_pts[0]) is equal to x.lb and the last + point (x_pts[-1]) is equal to x.ub + f_x_expr: pyomo expression + An expression for f(x) + pw_repn: str + This must be one of the valid strings for the peicewise representation to use (directly from the Piecewise + component). Use help(Piecewise) to learn more. + shape: FunctionShape + Specify the shape of the function. Valid values are minlp.FunctionShape.CONVEX or minlp.FunctionShape.CONCAVE + relaxation_side: RelaxationSide + Provide the desired side for the relaxation (OVER, UNDER, or BOTH) + large_eval_tol: float + To avoid numerical problems, if f_x_expr or its derivative evaluates to a value larger than large_eval_tol, + at a point in x_pts, then that point is skipped. + """ + assert shape in {FunctionShape.CONCAVE, FunctionShape.CONVEX} + assert relaxation_side in {RelaxationSide.UNDER, RelaxationSide.OVER} + if relaxation_side == RelaxationSide.UNDER: + assert shape == FunctionShape.CONCAVE + else: + assert shape == FunctionShape.CONVEX + + _eval = _FxExpr(expr=f_x_expr, x=x) + xlb = x_pts[0] + xub = x_pts[-1] + + check_var_pts(x, x_pts) + + if x.is_fixed(): + b.x_fixed_con = pyo.Constraint(expr=w == _eval(x.value)) + elif xlb == xub: + b.x_fixed_con = pyo.Constraint(expr=w == _eval(x.lb)) + else: + # Do the non-convex piecewise portion if shape=CONCAVE and relaxation_side=Under/BOTH + # or if shape=CONVEX and relaxation_side=Over/BOTH + pw_constr_type = None + if shape == FunctionShape.CONVEX and relaxation_side in {RelaxationSide.OVER, RelaxationSide.BOTH}: + pw_constr_type = 'UB' + _eval = _FxExpr(expr=f_x_expr + safety_tol, x=x) + if shape == FunctionShape.CONCAVE and relaxation_side in {RelaxationSide.UNDER, RelaxationSide.BOTH}: + pw_constr_type = 'LB' + _eval = _FxExpr(expr=f_x_expr - safety_tol, x=x) + + if pw_constr_type is not None: + # Build the piecewise side of the envelope + if x_pts[0] > -math.inf and x_pts[-1] < math.inf: + tmp_pts = list() + for _pt in x_pts: + try: + f = _eval(_pt) + if abs(f) >= large_eval_tol: + logger.warning(f'Skipping pt {_pt} for var {str(x)} because |{str(f_x_expr)}| ' + f'evaluated at {_pt} is larger than {large_eval_tol}') + continue + tmp_pts.append(_pt) + except (ZeroDivisionError, ValueError, OverflowError): + pass + if len(tmp_pts) >= 2 and tmp_pts[0] == x_pts[0] and tmp_pts[-1] == x_pts[-1]: + b.pw_linear_under_over = pyo.Piecewise(w, x, + pw_pts=tmp_pts, + pw_repn=pw_repn, + pw_constr_type=pw_constr_type, + f_rule=_func_wrapper(_eval) + ) + + +def pw_sin_relaxation(b, x, w, x_pts, relaxation_side=RelaxationSide.BOTH, safety_tol=1e-10): + """ + This function creates piecewise relaxations to relax "w=sin(x)" for -pi/2 <= x <= pi/2. + + Parameters + ---------- + b: pyo.Block + x: pyomo.core.base.var.SimpleVar or pyomo.core.base.var._GeneralVarData + The "x" variable in sin(x). The lower bound on x must greater than or equal to + -pi/2 and the upper bound on x must be less than or equal to pi/2. + w: pyomo.core.base.var.SimpleVar or pyomo.core.base.var._GeneralVarData + The auxillary variable replacing sin(x) + x_pts: Sequence[float] + A list of floating point numbers to define the points over which the piecewise + representation will be generated. This list must be ordered, and it is expected + that the first point (x_pts[0]) is equal to x.lb and the last point (x_pts[-1]) + is equal to x.ub + relaxation_side: minlp.RelaxationSide + Provide the desired side for the relaxation (OVER, UNDER, or BOTH) + safety_tol: float + amount to lift the overestimator or drop the underestimator. This is used to ensure none of the feasible + region is cut off by error in computing the over and under estimators. + """ + check_var_pts(x, x_pts) + expr = pyo.sin(x) + + xlb = x_pts[0] + xub = x_pts[-1] + + if x.is_fixed() or xlb == xub: + b.x_fixed_con = pyo.Constraint(expr=w == (pyo.value(expr))) + return + + if xlb < -np.pi / 2.0: + return + + if xub > np.pi / 2.0: + return + + OE_tangent_x, OE_tangent_slope, OE_tangent_intercept = _compute_sine_overestimator_tangent_point(xlb) + UE_tangent_x, UE_tangent_slope, UE_tangent_intercept = _compute_sine_underestimator_tangent_point(xub) + non_piecewise_overestimators_pts = [] + non_piecewise_underestimator_pts = [] + + if relaxation_side == RelaxationSide.OVER: + if OE_tangent_x < xub: + new_x_pts = [i for i in x_pts if i < OE_tangent_x] + new_x_pts.append(xub) + non_piecewise_overestimators_pts = [OE_tangent_x] + non_piecewise_overestimators_pts.extend(i for i in x_pts if i > OE_tangent_x) + x_pts = new_x_pts + elif relaxation_side == RelaxationSide.UNDER: + if UE_tangent_x > xlb: + new_x_pts = [xlb] + new_x_pts.extend(i for i in x_pts if i > UE_tangent_x) + non_piecewise_underestimator_pts = [i for i in x_pts if i < UE_tangent_x] + non_piecewise_underestimator_pts.append(UE_tangent_x) + x_pts = new_x_pts + + b.non_piecewise_overestimators = pyo.ConstraintList() + b.non_piecewise_underestimators = pyo.ConstraintList() + for pt in non_piecewise_overestimators_pts: + b.non_piecewise_overestimators.add(w <= math.sin(pt) + safety_tol + (x - pt) * math.cos(pt)) + for pt in non_piecewise_underestimator_pts: + b.non_piecewise_underestimators.add(w >= math.sin(pt) - safety_tol + (x - pt) * math.cos(pt)) + + intervals = [] + for i in range(len(x_pts)-1): + intervals.append((x_pts[i], x_pts[i+1])) + + b.interval_set = pyo.Set(initialize=range(len(intervals)), ordered=True) + b.x = pyo.Var(b.interval_set) + b.w = pyo.Var(b.interval_set) + if len(intervals) == 1: + b.lam = pyo.Param(b.interval_set, mutable=True) + b.lam[0].value = 1.0 + else: + b.lam = pyo.Var(b.interval_set, within=pyo.Binary) + b.x_lb = pyo.ConstraintList() + b.x_ub = pyo.ConstraintList() + b.x_sum = pyo.Constraint(expr=x == sum(b.x[i] for i in b.interval_set)) + b.w_sum = pyo.Constraint(expr=w == sum(b.w[i] for i in b.interval_set)) + b.lam_sum = pyo.Constraint(expr=sum(b.lam[i] for i in b.interval_set) == 1) + b.overestimators = pyo.ConstraintList() + b.underestimators = pyo.ConstraintList() + + for i, tup in enumerate(intervals): + x0 = tup[0] + x1 = tup[1] + + b.x_lb.add(x0 * b.lam[i] <= b.x[i]) + b.x_ub.add(b.x[i] <= x1 * b.lam[i]) + + # Overestimators + if relaxation_side in {RelaxationSide.OVER, RelaxationSide.BOTH}: + if x0 < 0 and x1 <= 0: + slope = (math.sin(x1) - math.sin(x0)) / (x1 - x0) + intercept = math.sin(x0) - slope * x0 + b.overestimators.add(b.w[i] <= slope * b.x[i] + (intercept + safety_tol) * b.lam[i]) + elif (x0 < 0) and (x1 > 0): + tangent_x, tangent_slope, tangent_intercept = _compute_sine_overestimator_tangent_point(x0) + if tangent_x <= x1: + b.overestimators.add(b.w[i] <= tangent_slope * b.x[i] + (tangent_intercept + safety_tol) * b.lam[i]) + b.overestimators.add(b.w[i] <= math.cos(x1) * b.x[i] + + (math.sin(x1) - x1 * math.cos(x1) + safety_tol) * b.lam[i]) + else: + slope = (math.sin(x1) - math.sin(x0)) / (x1 - x0) + intercept = math.sin(x0) - slope * x0 + b.overestimators.add(b.w[i] <= slope * b.x[i] + (intercept + safety_tol) * b.lam[i]) + else: + b.overestimators.add(b.w[i] <= math.cos(x0)*b.x[i] + + (math.sin(x0) - x0*math.cos(x0) + safety_tol)*b.lam[i]) + b.overestimators.add(b.w[i] <= math.cos(x1)*b.x[i] + + (math.sin(x1) - x1*math.cos(x1) + safety_tol)*b.lam[i]) + + # Underestimators + if relaxation_side in {RelaxationSide.UNDER, RelaxationSide.BOTH}: + if x0 >= 0 and x1 > 0: + slope = (math.sin(x1) - math.sin(x0)) / (x1 - x0) + intercept = math.sin(x0) - slope * x0 + b.underestimators.add(b.w[i] >= slope * b.x[i] + (intercept - safety_tol) * b.lam[i]) + elif (x1 > 0) and (x0 < 0): + tangent_x, tangent_slope, tangent_intercept = _compute_sine_underestimator_tangent_point(x1) + if tangent_x >= x0: + b.underestimators.add(b.w[i] >= tangent_slope*b.x[i] + (tangent_intercept - safety_tol)*b.lam[i]) + b.underestimators.add(b.w[i] >= math.cos(x0)*b.x[i] + + (math.sin(x0) - x0 * math.cos(x0) - safety_tol)*b.lam[i]) + else: + slope = (math.sin(x1) - math.sin(x0)) / (x1 - x0) + intercept = math.sin(x0) - slope * x0 + b.underestimators.add(b.w[i] >= slope * b.x[i] + (intercept - safety_tol) * b.lam[i]) + else: + b.underestimators.add(b.w[i] >= math.cos(x0)*b.x[i] + + (math.sin(x0) - x0 * math.cos(x0) - safety_tol)*b.lam[i]) + b.underestimators.add(b.w[i] >= math.cos(x1)*b.x[i] + + (math.sin(x1) - x1 * math.cos(x1) - safety_tol)*b.lam[i]) + + return x_pts + + +def pw_arctan_relaxation(b, x, w, x_pts, relaxation_side=RelaxationSide.BOTH, safety_tol=1e-10): + """ + This function creates piecewise relaxations to relax "w=sin(x)" for -pi/2 <= x <= pi/2. + + Parameters + ---------- + b: pyo.Block + x: pyomo.core.base.var.SimpleVar or pyomo.core.base.var._GeneralVarData + The "x" variable in sin(x). The lower bound on x must greater than or equal to + -pi/2 and the upper bound on x must be less than or equal to pi/2. + w: pyomo.core.base.var.SimpleVar or pyomo.core.base.var._GeneralVarData + The auxillary variable replacing sin(x) + x_pts: Sequence[float] + A list of floating point numbers to define the points over which the piecewise + representation will be generated. This list must be ordered, and it is expected + that the first point (x_pts[0]) is equal to x.lb and the last point (x_pts[-1]) + is equal to x.ub + relaxation_side: minlp.RelaxationSide + Provide the desired side for the relaxation (OVER, UNDER, or BOTH) + safety_tol: float + amount to lift the overestimator or drop the underestimator. This is used to ensure none of the feasible + region is cut off by error in computing the over and under estimators. + """ + check_var_pts(x, x_pts) + expr = pyo.atan(x) + _eval = _FxExpr(expr, x) + + xlb = x_pts[0] + xub = x_pts[-1] + + if x.is_fixed() or xlb == xub: + b.x_fixed_con = pyo.Constraint(expr=w == pyo.value(expr)) + return + + if xlb == -math.inf or xub == math.inf: + return + + OE_tangent_x, OE_tangent_slope, OE_tangent_intercept = _compute_arctan_overestimator_tangent_point(xlb) + UE_tangent_x, UE_tangent_slope, UE_tangent_intercept = _compute_arctan_underestimator_tangent_point(xub) + non_piecewise_overestimators_pts = [] + non_piecewise_underestimator_pts = [] + + if relaxation_side == RelaxationSide.OVER: + if OE_tangent_x < xub: + new_x_pts = [i for i in x_pts if i < OE_tangent_x] + new_x_pts.append(xub) + non_piecewise_overestimators_pts = [OE_tangent_x] + non_piecewise_overestimators_pts.extend(i for i in x_pts if i > OE_tangent_x) + x_pts = new_x_pts + elif relaxation_side == RelaxationSide.UNDER: + if UE_tangent_x > xlb: + new_x_pts = [xlb] + new_x_pts.extend(i for i in x_pts if i > UE_tangent_x) + non_piecewise_underestimator_pts = [i for i in x_pts if i < UE_tangent_x] + non_piecewise_underestimator_pts.append(UE_tangent_x) + x_pts = new_x_pts + + b.non_piecewise_overestimators = pyo.ConstraintList() + b.non_piecewise_underestimators = pyo.ConstraintList() + for pt in non_piecewise_overestimators_pts: + b.non_piecewise_overestimators.add(w <= math.atan(pt) + safety_tol + (x - pt) * _eval.deriv(pt)) + for pt in non_piecewise_underestimator_pts: + b.non_piecewise_underestimators.add(w >= math.atan(pt) - safety_tol + (x - pt) * _eval.deriv(pt)) + + intervals = [] + for i in range(len(x_pts)-1): + intervals.append((x_pts[i], x_pts[i+1])) + + b.interval_set = pyo.Set(initialize=range(len(intervals))) + b.x = pyo.Var(b.interval_set) + b.w = pyo.Var(b.interval_set) + if len(intervals) == 1: + b.lam = pyo.Param(b.interval_set, mutable=True) + b.lam[0].value = 1.0 + else: + b.lam = pyo.Var(b.interval_set, within=pyo.Binary) + b.x_lb = pyo.ConstraintList() + b.x_ub = pyo.ConstraintList() + b.x_sum = pyo.Constraint(expr=x == sum(b.x[i] for i in b.interval_set)) + b.w_sum = pyo.Constraint(expr=w == sum(b.w[i] for i in b.interval_set)) + b.lam_sum = pyo.Constraint(expr=sum(b.lam[i] for i in b.interval_set) == 1) + b.overestimators = pyo.ConstraintList() + b.underestimators = pyo.ConstraintList() + + for i, tup in enumerate(intervals): + x0 = tup[0] + x1 = tup[1] + + b.x_lb.add(x0 * b.lam[i] <= b.x[i]) + b.x_ub.add(b.x[i] <= x1 * b.lam[i]) + + # Overestimators + if relaxation_side in {RelaxationSide.OVER, RelaxationSide.BOTH}: + if x0 < 0 and x1 <= 0: + slope = (math.atan(x1) - math.atan(x0)) / (x1 - x0) + intercept = math.atan(x0) - slope * x0 + b.overestimators.add(b.w[i] <= slope * b.x[i] + (intercept + safety_tol) * b.lam[i]) + elif (x0 < 0) and (x1 > 0): + tangent_x, tangent_slope, tangent_intercept = _compute_arctan_overestimator_tangent_point(x0) + if tangent_x <= x1: + b.overestimators.add(b.w[i] <= tangent_slope * b.x[i] + (tangent_intercept + safety_tol) * b.lam[i]) + b.overestimators.add(b.w[i] <= _eval.deriv(x1)*b.x[i] + + (math.atan(x1) - x1*_eval.deriv(x1) + safety_tol)*b.lam[i]) + else: + slope = (math.atan(x1) - math.atan(x0)) / (x1 - x0) + intercept = math.atan(x0) - slope * x0 + b.overestimators.add(b.w[i] <= slope * b.x[i] + (intercept + safety_tol) * b.lam[i]) + else: + b.overestimators.add(b.w[i] <= _eval.deriv(x0)*b.x[i] + + (math.atan(x0) - x0*_eval.deriv(x0) + safety_tol)*b.lam[i]) + b.overestimators.add(b.w[i] <= _eval.deriv(x1)*b.x[i] + + (math.atan(x1) - x1*_eval.deriv(x1) + safety_tol)*b.lam[i]) + + # Underestimators + if relaxation_side in {RelaxationSide.UNDER, RelaxationSide.BOTH}: + if x0 >= 0 and x1 > 0: + slope = (math.atan(x1) - math.atan(x0)) / (x1 - x0) + intercept = math.atan(x0) - slope * x0 + b.underestimators.add(b.w[i] >= slope * b.x[i] + (intercept - safety_tol) * b.lam[i]) + elif (x1 > 0) and (x0 < 0): + tangent_x, tangent_slope, tangent_intercept = _compute_arctan_underestimator_tangent_point(x1) + if tangent_x >= x0: + b.underestimators.add(b.w[i] >= tangent_slope*b.x[i] + (tangent_intercept - safety_tol)*b.lam[i]) + b.underestimators.add(b.w[i] >= _eval.deriv(x0)*b.x[i] + + (math.atan(x0) - x0*_eval.deriv(x0) - safety_tol)*b.lam[i]) + else: + slope = (math.atan(x1) - math.atan(x0)) / (x1 - x0) + intercept = math.atan(x0) - slope * x0 + b.underestimators.add(b.w[i] >= slope * b.x[i] + (intercept - safety_tol) * b.lam[i]) + else: + b.underestimators.add(b.w[i] >= _eval.deriv(x0)*b.x[i] + + (math.atan(x0) - x0*_eval.deriv(x0) - safety_tol)*b.lam[i]) + b.underestimators.add(b.w[i] >= _eval.deriv(x1)*b.x[i] + + (math.atan(x1) - x1*_eval.deriv(x1) - safety_tol)*b.lam[i]) + + return x_pts + + +@declare_custom_block(name='PWUnivariateRelaxation') +class PWUnivariateRelaxationData(BasePWRelaxationData): + """ + A helper class for building and modifying piecewise relaxations of aux_var = f(x) where f(x) is either convex + or concave. + """ + + def __init__(self, component): + super().__init__(component) + self._xref = ComponentWeakRef(None) + self._aux_var_ref = ComponentWeakRef(None) + self._pw_repn = 'INC' + self._function_shape = FunctionShape.UNKNOWN + self._f_x_expr = None + self._secant: Optional[Union[ScalarConstraint, IndexedConstraint]] = None + self._secant_expr: Optional[LinearExpression] = None + self._secant_slope: Optional[Union[ScalarParam, IndexedParam]] = None + self._secant_intercept: Optional[Union[ScalarParam, IndexedParam]] = None + self._pw_secant = None + + @property + def _x(self): + return self._xref.get_component() + + @property + def _aux_var(self): + return self._aux_var_ref.get_component() + + def get_rhs_vars(self): + return self._x, + + def get_rhs_expr(self): + return self._f_x_expr + + def vars_with_bounds_in_relaxation(self): + res = list() + if self.relaxation_side == RelaxationSide.BOTH: + res.append(self._x) + elif self.relaxation_side == RelaxationSide.UNDER and not self.is_rhs_convex(): + res.append(self._x) + elif self.relaxation_side == RelaxationSide.OVER and not self.is_rhs_concave(): + res.append(self._x) + return res + + def set_input(self, x, aux_var, shape, f_x_expr, pw_repn='INC', relaxation_side=RelaxationSide.BOTH, + use_linear_relaxation=True, large_coef=1e5, small_coef=1e-10, safety_tol=1e-10): + """ + Parameters + ---------- + x: pyomo.core.base.var._GeneralVarData + The "x" variable in aux_var = f(x). + aux_var: pyomo.core.base.var._GeneralVarData + The auxillary variable replacing f(x) + shape: FunctionShape + Options are FunctionShape.CONVEX and FunctionShape.CONCAVE + f_x_expr: pyomo expression + The pyomo expression representing f(x) + pw_repn: str + This must be one of the valid strings for the piecewise representation to use (directly from the Piecewise + component). Use help(Piecewise) to learn more. + relaxation_side: RelaxationSide + Provide the desired side for the relaxation (OVER, UNDER, or BOTH) + use_linear_relaxation: bool + Specifies whether a linear or nonlinear relaxation should be used + """ + super().set_input(relaxation_side=relaxation_side, + use_linear_relaxation=use_linear_relaxation, + large_coef=large_coef, small_coef=small_coef, + safety_tol=safety_tol) + self._pw_repn = pw_repn + self._function_shape = shape + self._f_x_expr = f_x_expr + + self._xref.set_component(x) + self._aux_var_ref.set_component(aux_var) + bnds_list = _get_bnds_list(self._x) + self._partitions[self._x] = bnds_list + + def build(self, x, aux_var, shape, f_x_expr, pw_repn='INC', relaxation_side=RelaxationSide.BOTH, + use_linear_relaxation=True, large_coef=1e5, small_coef=1e-10, safety_tol=1e-10): + """ + Parameters + ---------- + x: pyomo.core.base.var._GeneralVarData + The "x" variable in aux_var = f(x). + aux_var: pyomo.core.base.var._GeneralVarData + The auxillary variable replacing f(x) + shape: FunctionShape + Options are FunctionShape.CONVEX and FunctionShape.CONCAVE + f_x_expr: pyomo expression + The pyomo expression representing f(x) + pw_repn: str + This must be one of the valid strings for the piecewise representation to use (directly from the Piecewise + component). Use help(Piecewise) to learn more. + relaxation_side: RelaxationSide + Provide the desired side for the relaxation (OVER, UNDER, or BOTH) + use_linear_relaxation: bool + Specifies whether a linear or nonlinear relaxation should be used + """ + self.set_input(x=x, aux_var=aux_var, shape=shape, f_x_expr=f_x_expr, pw_repn=pw_repn, + relaxation_side=relaxation_side, use_linear_relaxation=use_linear_relaxation, + large_coef=large_coef, small_coef=small_coef, safety_tol=safety_tol) + self.rebuild() + + def _remove_relaxation(self): + del self._secant, self._secant_slope, self._secant_intercept, self._pw_secant + self._secant = None + self._secant_expr = None + self._secant_slope = None + self._secant_intercept = None + self._pw_secant = None + + def remove_relaxation(self): + super().remove_relaxation() + self._remove_relaxation() + + def _needs_secant(self): + if self.relaxation_side == RelaxationSide.BOTH and (self.is_rhs_convex() or self.is_rhs_concave()): + return True + elif self.relaxation_side == RelaxationSide.UNDER and self.is_rhs_concave(): + return True + elif self.relaxation_side == RelaxationSide.OVER and self.is_rhs_convex(): + return True + else: + return False + + def rebuild(self, build_nonlinear_constraint=False, ensure_oa_at_vertices=True): + super().rebuild(build_nonlinear_constraint=build_nonlinear_constraint, + ensure_oa_at_vertices=ensure_oa_at_vertices) + if not build_nonlinear_constraint: + if self._check_valid_domain_for_relaxation(): + if self._needs_secant(): + if len(self._partitions[self._x]) == 2: + if self._secant is None: + self._remove_relaxation() + self._build_secant() + self._update_secant() + else: + self._remove_relaxation() + self._build_pw_secant() + else: + self._remove_relaxation() + else: + self._remove_relaxation() + + def _build_secant(self): + del self._secant_slope + del self._secant_intercept + del self._secant + del self._secant_expr + self._secant_slope = ScalarParam(mutable=True) + self._secant_intercept = ScalarParam(mutable=True) + e = LinearExpression(constant=self._secant_intercept, linear_coefs=[self._secant_slope], linear_vars=[self._x]) + self._secant_expr = e + if self.is_rhs_concave(): + self._secant = ScalarConstraint(expr=self._aux_var >= e) + elif self.is_rhs_convex(): + self._secant = ScalarConstraint(expr=self._aux_var <= e) + else: + raise RuntimeError('Function should be either convex or concave in order to build the secant') + + def _update_secant(self): + _eval = _FxExpr(self._f_x_expr, self._x) + assert len(self._partitions[self._x]) == 2 + + try: + x1 = self._partitions[self._x][0] + x2 = self._partitions[self._x][1] + if x1 == x2: + slope = 0 + intercept = _eval(x1) + else: + y1 = _eval(x1) + y2 = _eval(x2) + slope = (y2 - y1) / (x2 - x1) + intercept = y2 - slope*x2 + err_message = None + except (ZeroDivisionError, OverflowError, ValueError) as e: + slope = None + intercept = None + err_message = str(e) + if err_message is not None: + logger.debug(f'Encountered exception when adding secant for "{self._get_pprint_string()}"; Error message: {err_message}') + self._remove_relaxation() + else: + self._secant_slope._value = slope + self._secant_intercept._value = intercept + if self.is_rhs_concave(): + rel_side = RelaxationSide.UNDER + else: + rel_side = RelaxationSide.OVER + success, bad_var, bad_coef, err_msg = _check_cut(self._secant_expr, too_small=self.small_coef, + too_large=self.large_coef, relaxation_side=rel_side, + safety_tol=self.safety_tol) + if not success: + self._log_bad_cut(bad_var, bad_coef, err_msg) + self._secant.deactivate() + else: + self._secant.activate() + + def _build_pw_secant(self): + del self._pw_secant + self._pw_secant = pe.Block(concrete=True) + if self.is_rhs_convex(): + _pw_univariate_relaxation(b=self._pw_secant, x=self._x, w=self._aux_var, x_pts=self._partitions[self._x], + f_x_expr=self._f_x_expr, pw_repn=self._pw_repn, shape=FunctionShape.CONVEX, + relaxation_side=RelaxationSide.OVER, large_eval_tol=self.large_coef, + safety_tol=self.safety_tol) + else: + _pw_univariate_relaxation(b=self._pw_secant, x=self._x, w=self._aux_var, x_pts=self._partitions[self._x], + f_x_expr=self._f_x_expr, pw_repn=self._pw_repn, shape=FunctionShape.CONCAVE, + relaxation_side=RelaxationSide.UNDER, large_eval_tol=self.large_coef, + safety_tol=self.safety_tol) + + def add_partition_point(self, value=None): + """ + This method adds one point to the partitioning of x. If value is not + specified, a single point will be added to the partitioning of x at the current value of x. If value is + specified, then value is added to the partitioning of x. + + Parameters + ---------- + value: float + The point to be added to the partitioning of x. + """ + self._add_partition_point(self._x, value) + + def is_rhs_convex(self): + """ + Returns True if linear underestimators do not need binaries. Otherwise, returns False. + + Returns + ------- + bool + """ + return self._function_shape == FunctionShape.CONVEX + + def is_rhs_concave(self): + """ + Returns True if linear overestimators do not need binaries. Otherwise, returns False. + + Returns + ------- + bool + """ + return self._function_shape == FunctionShape.CONCAVE + + @property + def use_linear_relaxation(self): + return self._use_linear_relaxation + + @use_linear_relaxation.setter + def use_linear_relaxation(self, val): + self._use_linear_relaxation = val + + +@declare_custom_block(name='CustomUnivariateBaseRelaxation') +class CustomUnivariateBaseRelaxationData(PWUnivariateRelaxationData): + """ + A helper class for building and modifying piecewise relaxations of aux_var = x**2. + """ + + def _rhs_func(self, x): + raise NotImplementedError('This should be implemented by a derived class') + + def set_input(self, x, aux_var, pw_repn='INC', relaxation_side=RelaxationSide.BOTH, + use_linear_relaxation=True, large_coef=1e5, small_coef=1e-10, safety_tol=1e-10): + """ + Parameters + ---------- + x: pyomo.core.base.var._GeneralVarData + The "x" variable in aux_var = f(x). + aux_var: pyomo.core.base.var._GeneralVarData + The auxillary variable replacing f(x) + pw_repn: str + This must be one of the valid strings for the piecewise representation to use (directly from the Piecewise + component). Use help(Piecewise) to learn more. + relaxation_side: RelaxationSide + Provide the desired side for the relaxation (OVER, UNDER, or BOTH) + use_linear_relaxation: bool + Specifies whether a linear or nonlinear relaxation should be used + """ + super().set_input(x=x, aux_var=aux_var, shape=FunctionShape.UNKNOWN, + f_x_expr=self._rhs_func(x), pw_repn=pw_repn, + relaxation_side=relaxation_side, + use_linear_relaxation=use_linear_relaxation, + large_coef=large_coef, small_coef=small_coef, + safety_tol=safety_tol) + + def build(self, x, aux_var, pw_repn='INC', relaxation_side=RelaxationSide.BOTH, + use_linear_relaxation=True, large_coef=1e5, small_coef=1e-10, safety_tol=1e-10): + """ + Parameters + ---------- + x: pyomo.core.base.var._GeneralVarData + The "x" variable in aux_var = f(x). + aux_var: pyomo.core.base.var._GeneralVarData + The auxillary variable replacing f(x) + pw_repn: str + This must be one of the valid strings for the piecewise representation to use (directly from the Piecewise + component). Use help(Piecewise) to learn more. + relaxation_side: RelaxationSide + Provide the desired side for the relaxation (OVER, UNDER, or BOTH) + use_linear_relaxation: bool + Specifies whether a linear or nonlinear relaxation should be used + """ + self.set_input(x=x, aux_var=aux_var, pw_repn=pw_repn, relaxation_side=relaxation_side, + use_linear_relaxation=use_linear_relaxation, large_coef=large_coef, small_coef=small_coef, + safety_tol=safety_tol) + self.rebuild() + + +@declare_custom_block(name='PWXSquaredRelaxation') +class PWXSquaredRelaxationData(CustomUnivariateBaseRelaxationData): + """ + A helper class for building and modifying piecewise relaxations of aux_var = x**2. + """ + def _rhs_func(self, x): + return x**2 + + def is_rhs_convex(self): + return True + + +@declare_custom_block(name='PWCosRelaxation') +class PWCosRelaxationData(CustomUnivariateBaseRelaxationData): + """ + A helper class for building and modifying piecewise relaxations of w = cos(x) for -pi/2 <= x <= pi/2. + """ + + def __init__(self, component): + super().__init__(component) + self._last_concave = None + + def rebuild(self, build_nonlinear_constraint=False, ensure_oa_at_vertices=True): + current_concave = self.is_rhs_concave() + if current_concave != self._last_concave: + self._needs_rebuilt = True + self._last_concave = current_concave + super().rebuild(build_nonlinear_constraint=build_nonlinear_constraint, + ensure_oa_at_vertices=ensure_oa_at_vertices) + + def _rhs_func(self, x): + return pe.cos(x) + + def is_rhs_concave(self): + lb, ub = tuple(_get_bnds_list(self._x)) + if lb >= -math.pi/2 and ub <= math.pi/2: + return True + else: + return False + + +@declare_custom_block(name='SinArctanBaseRelaxation') +class SinArctanBaseRelaxationData(CustomUnivariateBaseRelaxationData): + """ + A helper class for building and modifying piecewise relaxations of w = sin(x) for -pi/2 <= x <= pi/2. + """ + + def _rhs_func(self, x): + raise NotImplementedError('This should be implemented by a derived class') + + def __init__(self, component): + super().__init__(component) + self._secant_index = None + self._secant_exprs = None + self._last_convex = None + self._last_concave = None + + def _remove_relaxation(self): + super()._remove_relaxation() + del self._secant_index + del self._secant_exprs + self._secant_index = None + self._secant_exprs = None + + def _pw_func(self): + raise NotImplementedError('This should be implemented by a derived class') + + def _underestimator_func(self): + raise NotImplementedError('This should be implemented by a derived class') + + def _overestimator_func(self): + raise NotImplementedError('This should be implemented by a derived class') + + def rebuild(self, build_nonlinear_constraint=False, ensure_oa_at_vertices=True): + current_convex = self.is_rhs_convex() + current_concave = self.is_rhs_concave() + if current_convex != self._last_convex or current_concave != self._last_concave: + self._needs_rebuilt = True + self._last_convex = current_convex + self._last_concave = current_concave + super().rebuild(build_nonlinear_constraint=build_nonlinear_constraint, + ensure_oa_at_vertices=ensure_oa_at_vertices) + if not build_nonlinear_constraint: + if self._check_valid_domain_for_relaxation(): + if (not self.is_rhs_convex()) and (not self.is_rhs_concave()): + if len(self._partitions[self._x]) == 2: + if self._secant is None: + self._remove_relaxation() + self._build_relaxation() + self._update_relaxation() + else: + self._remove_relaxation() + del self._pw_secant + self._pw_secant = pe.Block(concrete=True) + self._pw_func()(b=self._pw_secant, x=self._x, w=self._aux_var, x_pts=self._partitions[self._x], + relaxation_side=self.relaxation_side, + safety_tol=self.safety_tol) + else: + self._remove_relaxation() + + def _build_relaxation(self): + del self._secant_index, self._secant_slope, self._secant_intercept, self._secant + self._secant_index = pe.Set(initialize=[0, 1, 2, 3]) + self._secant_exprs = dict() + self._secant_slope = IndexedParam(self._secant_index, mutable=True) + self._secant_intercept = IndexedParam(self._secant_index, mutable=True) + self._secant = IndexedConstraint(self._secant_index) + if self.relaxation_side in {RelaxationSide.BOTH, RelaxationSide.UNDER}: + for ndx in [0, 1]: + e = LinearExpression(constant=self._secant_intercept[ndx], + linear_coefs=[self._secant_slope[ndx]], linear_vars=[self._x]) + self._secant_exprs[ndx] = e + self._secant[ndx] = self._aux_var >= e + if self.relaxation_side in {RelaxationSide.BOTH, RelaxationSide.OVER}: + for ndx in [2, 3]: + e = LinearExpression(constant=self._secant_intercept[ndx], + linear_coefs=[self._secant_slope[ndx]], linear_vars=[self._x]) + self._secant_exprs[ndx] = e + self._secant[ndx] = self._aux_var <= e + + def _check_expr(self, ndx): + if ndx in {0, 1}: + rel_side = RelaxationSide.UNDER + else: + rel_side = RelaxationSide.OVER + success, bad_var, bad_coef, err_msg = _check_cut(self._secant_exprs[ndx], too_small=self.small_coef, + too_large=self.large_coef, relaxation_side=rel_side, + safety_tol=self.safety_tol) + if not success: + self._log_bad_cut(bad_var, bad_coef, err_msg) + self._secant[ndx].deactivate() + else: + self._secant[ndx].activate() + + def _update_relaxation(self): + xlb, xub = _get_bnds_tuple(self._x) + _eval = _FxExpr(self.get_rhs_expr(), self._x) + if self.relaxation_side in {RelaxationSide.BOTH, RelaxationSide.UNDER}: + tangent_x, tangent_slope, tangent_int = self._underestimator_func()(xub) + if tangent_x >= xlb: + self._secant_slope[0]._value = tangent_slope + self._secant_intercept[0]._value = tangent_int + self._secant_slope[1]._value = _eval.deriv(xlb) + self._secant_intercept[1]._value = _eval(xlb) - xlb * _eval.deriv(xlb) + self._check_expr(0) + self._check_expr(1) + else: + y1 = _eval(xlb) + y2 = _eval(xub) + slope = (y2 - y1) / (xub - xlb) + intercept = y2 - slope * xub + self._secant_slope[0]._value = slope + self._secant_intercept[0]._value = intercept + self._check_expr(0) + self._secant[1].deactivate() + + if self.relaxation_side in {RelaxationSide.BOTH, RelaxationSide.OVER}: + tangent_x, tangent_slope, tangent_int = self._overestimator_func()(xlb) + if tangent_x <= xub: + self._secant_slope[2]._value = tangent_slope + self._secant_intercept[2]._value = tangent_int + self._secant_slope[3]._value = _eval.deriv(xub) + self._secant_intercept[3]._value = _eval(xub) - xub * _eval.deriv(xub) + self._check_expr(2) + self._check_expr(3) + else: + y1 = _eval(xlb) + y2 = _eval(xub) + slope = (y2 - y1) / (xub - xlb) + intercept = y2 - slope * xub + self._secant_slope[2]._value = slope + self._secant_intercept[2]._value = intercept + self._check_expr(2) + self._secant[3].deactivate() + + +@declare_custom_block(name='PWSinRelaxation') +class PWSinRelaxationData(SinArctanBaseRelaxationData): + """ + A helper class for building and modifying piecewise relaxations of w = sin(x) for -pi/2 <= x <= pi/2. + """ + + def _rhs_func(self, x): + return pe.sin(x) + + def _check_valid_domain_for_relaxation(self) -> bool: + lb, ub = _get_bnds_tuple(self._x) + if lb >= -math.pi / 2 and ub <= math.pi / 2: + return True + return False + + def _pw_func(self): + return pw_sin_relaxation + + def _underestimator_func(self): + return _compute_sine_underestimator_tangent_point + + def _overestimator_func(self): + return _compute_sine_overestimator_tangent_point + + def is_rhs_convex(self): + """ + Returns True if linear underestimators do not need binaries. Otherwise, returns False. + + Returns + ------- + bool + """ + lb, ub = tuple(_get_bnds_list(self._x)) + if lb >= -math.pi / 2 and ub <= 0: + return True + return False + + def is_rhs_concave(self): + """ + Returns True if linear overestimators do not need binaries. Otherwise, returns False. + + Returns + ------- + bool + """ + lb, ub = tuple(_get_bnds_list(self._x)) + if lb >= 0 and ub <= math.pi / 2: + return True + return False + + +@declare_custom_block(name='PWArctanRelaxation') +class PWArctanRelaxationData(SinArctanBaseRelaxationData): + """ + A helper class for building and modifying piecewise relaxations of w = arctan(x). + """ + + def _rhs_func(self, x): + return pe.atan(x) + + def _pw_func(self): + return pw_arctan_relaxation + + def _underestimator_func(self): + return _compute_arctan_underestimator_tangent_point + + def _overestimator_func(self): + return _compute_arctan_overestimator_tangent_point + + def is_rhs_convex(self): + """ + Returns True if linear underestimators do not need binaries. Otherwise, returns False. + + Returns + ------- + bool + """ + lb, ub = tuple(_get_bnds_list(self._x)) + if ub <= 0: + return True + return False + + def is_rhs_concave(self): + """ + Returns True if linear overestimators do not need binaries. Otherwise, returns False. + + Returns + ------- + bool + """ + lb, ub = tuple(_get_bnds_list(self._x)) + if lb >= 0: + return True + return False diff --git a/pyomo/contrib/coramin/third_party/__init__.py b/pyomo/contrib/coramin/third_party/__init__.py new file mode 100644 index 00000000000..53ac6c77e5d --- /dev/null +++ b/pyomo/contrib/coramin/third_party/__init__.py @@ -0,0 +1 @@ +from .minlplib_tools import get_minlplib_instancedata, filter_minlplib_instances, get_minlplib diff --git a/pyomo/contrib/coramin/third_party/minlplib_tools.py b/pyomo/contrib/coramin/third_party/minlplib_tools.py new file mode 100644 index 00000000000..a39054bd5fb --- /dev/null +++ b/pyomo/contrib/coramin/third_party/minlplib_tools.py @@ -0,0 +1,349 @@ +import os +from zipfile import ZipFile +from pyomo.common import fileutils +from pyomo.common import download +import enum +import math +import csv +from collections.abc import Iterable +import logging + + +logger = logging.getLogger(__name__) + + +def get_minlplib_instancedata(target_filename=None): + """ + Download instancedata.csv from MINLPLib which can be used to get statistics on the problems from minlplib. + + Parameters + ---------- + target_filename: str + The full path, including the filename for where to place the downloaded + file. The default will be a directory called minlplib in the current + working directory and a filename of instancedata.csv. + """ + if target_filename is None: + target_filename = os.path.join(os.getcwd(), 'minlplib', 'instancedata.csv') + download_dir = os.path.dirname(target_filename) + + if os.path.exists(target_filename): + raise ValueError('A file named {filename} already exists.'.format(filename=target_filename)) + + if not os.path.exists(download_dir): + os.makedirs(download_dir) + + downloader = download.FileDownloader() + downloader.set_destination_filename(target_filename) + downloader.get_text_file('http://www.minlplib.org/instancedata.csv') + + +def _process_acceptable_arg(name, arg, default): + if arg is None: + return default + if isinstance(arg, str): + if arg not in default: + raise ValueError("Unrecognized argument for %s: %s" % (name, arg)) + return set([arg]) + if isinstance(arg, Iterable): + ans = set(str(_) for _ in arg) + if not ans.issubset(default): + unknown = default - ans + raise ValueError("Unrecognized argument for %s: %s" % (name, unknown)) + return ans + if type(arg) == bool: + if str(arg) in default: + return set([str(arg)]) + raise ValueError('unrecognized type for %s: %s' % (name, type(arg))) + + +def _check_int_arg(arg, _min, _max, arg_name, case_name): + if arg < _min or arg > _max: + logger.debug('excluding {case_name} due to {arg_name}'.format(case_name=case_name, arg_name=arg_name)) + return True + return False + + +def _check_acceptable(arg, acceptable_set, arg_name, case_name): + if arg not in acceptable_set: + logger.debug('excluding {case_name} due to {arg_name}'.format(case_name=case_name, arg_name=arg_name)) + return True + return False + + +def filter_minlplib_instances(instancedata_filename=None, + min_nvars=0, max_nvars=math.inf, + min_nbinvars=0, max_nbinvars=math.inf, + min_nintvars=0, max_nintvars=math.inf, + min_nnlvars=0, max_nnlvars=math.inf, + min_nnlbinvars=0, max_nnlbinvars=math.inf, + min_nnlintvars=0, max_nnlintvars=math.inf, + min_nobjnz=0, max_nobjnz=math.inf, + min_nobjnlnz=0, max_nobjnlnz=math.inf, + min_ncons=0, max_ncons=math.inf, + min_nlincons=0, max_nlincons=math.inf, + min_nquadcons=0, max_nquadcons=math.inf, + min_npolynomcons=0, max_npolynomcons=math.inf, + min_nsignomcons=0, max_nsignomcons=math.inf, + min_ngennlcons=0, max_ngennlcons=math.inf, + min_njacobiannz=0, max_njacobiannz=math.inf, + min_njacobiannlnz=0, max_njacobiannlnz=math.inf, + min_nlaghessiannz=0, max_nlaghessiannz=math.inf, + min_nlaghessiandiagnz=0, max_nlaghessiandiagnz=math.inf, + min_nsemi=0, max_nsemi=math.inf, + min_nnlsemi=0, max_nnlsemi=math.inf, + min_nsos1=0, max_nsos1=math.inf, + min_nsos2=0, max_nsos2=math.inf, + acceptable_formats=None, + acceptable_probtype=None, + acceptable_objtype=None, + acceptable_objcurvature=None, + acceptable_conscurvature=None, + acceptable_convex=None): + """ + This function filters problems from MINLPLib based on + instancedata.csv from MINLPLib and the conditions specified + through the function arguments. The function argument names + correspond to column headings from instancedata.csv. The + arguments starting with min or max require int or float inputs. + The arguments starting with acceptable require either a + string or an iterable of strings. See the MINLPLib documentation + for acceptable values. + """ + if instancedata_filename is None: + instancedata_filename = os.path.join(os.getcwd(), 'minlplib', 'instancedata.csv') + + if not os.path.exists(instancedata_filename): + raise RuntimeError('{filename} does not exist. Please use get_minlplib_instancedata() first or specify the location of the MINLPLib instancedata.csv with the instancedata_filename argument.'.format(filename=instancedata_filename)) + + acceptable_formats = _process_acceptable_arg( + 'acceptable_formats', + acceptable_formats, + set(['ams', 'gms', 'lp', 'mod', 'nl', 'osil', 'pip']) + ) + + default_acceptable_probtype = set() + for pre in ['B', 'I', 'MI', 'MB', 'S', '']: + for post in ['NLP', 'QCQP', 'QP', 'QCP', 'P']: + default_acceptable_probtype.add(pre + post) + acceptable_probtype = _process_acceptable_arg( + 'acceptable_probtype', + acceptable_probtype, + default_acceptable_probtype + ) + + acceptable_objtype = _process_acceptable_arg( + 'acceptable_objtype', + acceptable_objtype, + set(['constant', 'linear', 'quadratic', 'polynomial', 'signomial', 'nonlinear']) + ) + + acceptable_objcurvature = _process_acceptable_arg( + 'acceptable_objcurvature', + acceptable_objcurvature, + set(['linear', 'convex', 'concave', 'indefinite', 'nonconvex', 'nonconcave', 'unknown']) + ) + + acceptable_conscurvature = _process_acceptable_arg( + 'acceptable_conscurvature', + acceptable_conscurvature, + set(['linear', 'convex', 'concave', 'indefinite', 'nonconvex', 'nonconcave', 'unknown']) + ) + + acceptable_convex = _process_acceptable_arg( + 'acceptable_convex', + acceptable_convex, + set(['True', 'False', '']) + ) + + int_arg_name_list = [ + 'nvars', + 'nbinvars', + 'nintvars', + 'nnlvars', + 'nnlbinvars', + 'nnlintvars', + 'nobjnz', + 'nobjnlnz', + 'ncons', + 'nlincons', + 'nquadcons', + 'npolynomcons', + 'nsignomcons', + 'ngennlcons', + 'njacobiannz', + 'njacobiannlnz', + 'nlaghessiannz', + 'nlaghessiandiagnz', + 'nsemi', + 'nnlsemi', + 'nsos1', + 'nsos2', + ] + min_list = [ + min_nvars, + min_nbinvars, + min_nintvars, + min_nnlvars, + min_nnlbinvars, + min_nnlintvars, + min_nobjnz, + min_nobjnlnz, + min_ncons, + min_nlincons, + min_nquadcons, + min_npolynomcons, + min_nsignomcons, + min_ngennlcons, + min_njacobiannz, + min_njacobiannlnz, + min_nlaghessiannz, + min_nlaghessiandiagnz, + min_nsemi, + min_nnlsemi, + min_nsos1, + min_nsos2, + ] + max_list = [ + max_nvars, + max_nbinvars, + max_nintvars, + max_nnlvars, + max_nnlbinvars, + max_nnlintvars, + max_nobjnz, + max_nobjnlnz, + max_ncons, + max_nlincons, + max_nquadcons, + max_npolynomcons, + max_nsignomcons, + max_ngennlcons, + max_njacobiannz, + max_njacobiannlnz, + max_nlaghessiannz, + max_nlaghessiandiagnz, + max_nsemi, + max_nnlsemi, + max_nsos1, + max_nsos2, + ] + + acceptable_arg_name_list = [ + 'probtype', + 'objtype', + 'objcurvature', + 'conscurvature', + 'convex' + ] + acceptable_set_list = [ + acceptable_probtype, + acceptable_objtype, + acceptable_objcurvature, + acceptable_conscurvature, + acceptable_convex + ] + + with open(instancedata_filename, 'r') as csv_file: + reader = csv.reader(csv_file, delimiter=';') + headings = {column: ndx for ndx, column in enumerate(next(reader))} + rows = [row for row in reader] + + cases = list() + for ndx, row in enumerate(rows): + if len(row) == 0: + continue + + case_name = row[headings['name']] + + available_formats = row[headings['formats']] + available_formats = available_formats.replace('set([', '') + available_formats = available_formats.replace('])', '') + available_formats = available_formats.replace('{', '') + available_formats = available_formats.replace('}', '') + available_formats = available_formats.replace(' ', '') + available_formats = available_formats.replace("'", '') + available_formats = available_formats.split(',') + available_formats = set(available_formats) + + should_continue = False + + if len(acceptable_formats.intersection(available_formats)) == 0: + logger.debug('excluding {case} due to available_formats'.format(case=case_name)) + should_continue = True + + for ndx, acceptable_arg_name in enumerate(acceptable_arg_name_list): + acceptable_set = acceptable_set_list[ndx] + arg = row[headings[acceptable_arg_name]] + if _check_acceptable(arg=arg, + acceptable_set=acceptable_set, + arg_name=acceptable_arg_name, + case_name=case_name): + should_continue = True + + for ndx, arg_name in enumerate(int_arg_name_list): + _min = min_list[ndx] + _max = max_list[ndx] + arg = int(row[headings[arg_name]]) + if _check_int_arg(arg=arg, + _min=_min, + _max=_max, + arg_name=arg_name, + case_name=case_name): + should_continue = True + + if should_continue: + continue + + cases.append(case_name) + + return cases + + +def get_minlplib(download_dir=None, format='osil', problem_name=None): + """ + Download MINLPLib + + Parameters + ---------- + download_dir: str + The directory in which to place the downloaded files. The default will be a + current_working_directory/minlplib/file_format/. + format: str + The file format requested. Options are ams, gms, lp, mod, nl, osil, and pip + problem_name: None or str + If problem_name is None, then the entire zip file will be downloaded + and extracted (all problems with the specified format). If a single problem + needs to be downloaded, then the name of the problem can be specified. + This can be significantly faster than downloading all of the problems. + However, individual problems are not compressed, so downloading multiple + individual problems can quickly become expensive. + """ + if download_dir is None: + download_dir = os.path.join(os.getcwd(), 'minlplib', format) + + if problem_name is None: + if os.path.exists(download_dir): + raise ValueError( + 'The specified download_dir already exists: ' + download_dir) + + os.makedirs(download_dir) + downloader = download.FileDownloader() + zip_dirname = os.path.join(download_dir, 'minlplib_'+format) + downloader.set_destination_filename(zip_dirname) + downloader.get_zip_archive('http://www.minlplib.org/minlplib_'+format+'.zip') + for i in os.listdir(os.path.join(download_dir, 'minlplib_'+format, 'minlplib', format)): + os.rename(os.path.join(download_dir, 'minlplib_'+format, 'minlplib', format, i), os.path.join(download_dir, i)) + os.rmdir(os.path.join(download_dir, 'minlplib_'+format, 'minlplib', format)) + os.rmdir(os.path.join(download_dir, 'minlplib_'+format, 'minlplib')) + os.rmdir(os.path.join(download_dir, 'minlplib_'+format)) + else: + if not os.path.exists(download_dir): + os.makedirs(download_dir) + target_filename = os.path.join(download_dir, problem_name + '.' + format) + if os.path.exists(target_filename): + raise ValueError(f'The target filename ({target_filename}) already exists') + downloader = download.FileDownloader() + downloader.set_destination_filename(target_filename) + downloader.get_binary_file('http://www.minlplib.org/'+format+'/'+problem_name+'.'+format) + diff --git a/pyomo/contrib/coramin/third_party/tests/test_minlplib_tools.py b/pyomo/contrib/coramin/third_party/tests/test_minlplib_tools.py new file mode 100644 index 00000000000..c5c442f1284 --- /dev/null +++ b/pyomo/contrib/coramin/third_party/tests/test_minlplib_tools.py @@ -0,0 +1,206 @@ +import coramin +import unittest +import os +from pyomo.common.fileutils import this_file_dir +from urllib.request import urlopen +from socket import timeout + + +try: + urlopen('http://www.minlplib.org', timeout=3) +except timeout: + raise unittest.SkipTest('an internet connection is required to test minlplib_tools') + + +class TestMINLPLibTools(unittest.TestCase): + def test_get_minlplib_instancedata(self): + current_dir = os.getcwd() + coramin.third_party.get_minlplib_instancedata() + self.assertTrue(os.path.exists(os.path.join(current_dir, 'minlplib', 'instancedata.csv'))) + cases = coramin.third_party.filter_minlplib_instances() + self.assertEqual(len(cases), 1595) + os.remove(os.path.join(current_dir, 'minlplib', 'instancedata.csv')) + os.rmdir(os.path.join(current_dir, 'minlplib')) + + def test_filter_minlplib_instances(self): + current_dir = this_file_dir() + coramin.third_party.get_minlplib_instancedata(target_filename=os.path.join(current_dir, 'minlplib', 'instancedata.csv')) + + total_cases = 1595 + cases = coramin.third_party.filter_minlplib_instances(instancedata_filename=os.path.join(current_dir, 'minlplib', 'instancedata.csv'), + acceptable_formats='osil', + acceptable_probtype='QCQP', + min_njacobiannz=1000, + max_njacobiannz=10000) + self.assertEqual(len(cases), 6) # regression + + cases = coramin.third_party.filter_minlplib_instances(instancedata_filename=os.path.join(current_dir, 'minlplib', 'instancedata.csv'), + acceptable_formats=['osil', 'gms']) + self.assertEqual(len(cases), total_cases) # unit + + cases = coramin.third_party.filter_minlplib_instances(instancedata_filename=os.path.join(current_dir, 'minlplib', 'instancedata.csv')) + self.assertEqual(len(cases), total_cases) # unit + + cases = coramin.third_party.filter_minlplib_instances(instancedata_filename=os.path.join(current_dir, 'minlplib', 'instancedata.csv'), + acceptable_probtype=['QCQP', 'MIQCQP', 'MBQCQP']) + self.assertEqual(len(cases), 56) # regression + + cases = coramin.third_party.filter_minlplib_instances(instancedata_filename=os.path.join(current_dir, 'minlplib', 'instancedata.csv'), + acceptable_objtype='linear', + acceptable_objcurvature='linear', + acceptable_conscurvature='convex', + acceptable_convex=True) + self.assertEqual(len(cases), 280) # unit + + cases = coramin.third_party.filter_minlplib_instances(instancedata_filename=os.path.join(current_dir, 'minlplib', 'instancedata.csv'), + acceptable_convex=[True]) + self.assertEqual(len(cases), 377) # unit + + cases = coramin.third_party.filter_minlplib_instances(instancedata_filename=os.path.join(current_dir, 'minlplib', 'instancedata.csv'), + min_nvars=2, max_nvars=200000) + self.assertEqual(len(cases), total_cases-16-1) # unit + + cases = coramin.third_party.filter_minlplib_instances(instancedata_filename=os.path.join(current_dir, 'minlplib', 'instancedata.csv'), + max_nbinvars=31000) + self.assertEqual(len(cases), total_cases-1) # unit + + cases = coramin.third_party.filter_minlplib_instances(instancedata_filename=os.path.join(current_dir, 'minlplib', 'instancedata.csv'), + max_nintvars=1999) + self.assertEqual(len(cases), total_cases-1) # unit + + cases = coramin.third_party.filter_minlplib_instances(instancedata_filename=os.path.join(current_dir, 'minlplib', 'instancedata.csv'), + max_ncons=164000) + self.assertEqual(len(cases), total_cases-1) # unit + + cases = coramin.third_party.filter_minlplib_instances(instancedata_filename=os.path.join(current_dir, 'minlplib', 'instancedata.csv'), + max_nsemi=13) + self.assertEqual(len(cases), total_cases) # unit + + cases = coramin.third_party.filter_minlplib_instances(instancedata_filename=os.path.join(current_dir, 'minlplib', 'instancedata.csv'), + max_nsos1=0, max_nsos2=0) + self.assertEqual(len(cases), total_cases-6) # unit + + cases = coramin.third_party.filter_minlplib_instances(instancedata_filename=os.path.join(current_dir, 'minlplib', 'instancedata.csv'), + max_nnlvars=199998) + self.assertEqual(len(cases), total_cases-1) # unit + + cases = coramin.third_party.filter_minlplib_instances(instancedata_filename=os.path.join(current_dir, 'minlplib', 'instancedata.csv'), + max_nnlbinvars=23867) + self.assertEqual(len(cases), total_cases-1) # unit + + cases = coramin.third_party.filter_minlplib_instances(instancedata_filename=os.path.join(current_dir, 'minlplib', 'instancedata.csv'), + max_nnlintvars=1999) + self.assertEqual(len(cases), total_cases-1) # unit + + cases = coramin.third_party.filter_minlplib_instances(instancedata_filename=os.path.join(current_dir, 'minlplib', 'instancedata.csv'), + max_nobjnz=99997) + self.assertEqual(len(cases), total_cases-1) # unit + + cases = coramin.third_party.filter_minlplib_instances(instancedata_filename=os.path.join(current_dir, 'minlplib', 'instancedata.csv'), + max_nobjnlnz=99997) + self.assertEqual(len(cases), total_cases-1) # unit + + cases = coramin.third_party.filter_minlplib_instances(instancedata_filename=os.path.join(current_dir, 'minlplib', 'instancedata.csv'), + max_nlincons=164319) + self.assertEqual(len(cases), total_cases-1) # unit + + cases = coramin.third_party.filter_minlplib_instances(instancedata_filename=os.path.join(current_dir, 'minlplib', 'instancedata.csv'), + max_nquadcons=139999) + self.assertEqual(len(cases), total_cases-1) # unit + + cases = coramin.third_party.filter_minlplib_instances(instancedata_filename=os.path.join(current_dir, 'minlplib', 'instancedata.csv'), + max_npolynomcons=13975) + self.assertEqual(len(cases), total_cases-1) # unit + + cases = coramin.third_party.filter_minlplib_instances(instancedata_filename=os.path.join(current_dir, 'minlplib', 'instancedata.csv'), + max_nsignomcons=801) + self.assertEqual(len(cases), total_cases-1) # unit + + cases = coramin.third_party.filter_minlplib_instances(instancedata_filename=os.path.join(current_dir, 'minlplib', 'instancedata.csv'), + max_ngennlcons=13975) + self.assertEqual(len(cases), total_cases-2) # unit + + cases = coramin.third_party.filter_minlplib_instances(instancedata_filename=os.path.join(current_dir, 'minlplib', 'instancedata.csv'), + max_njacobiannlnz=1623023) + self.assertEqual(len(cases), total_cases-1) # unit + + cases = coramin.third_party.filter_minlplib_instances(instancedata_filename=os.path.join(current_dir, 'minlplib', 'instancedata.csv'), + max_nlaghessiannz=1825419) + self.assertEqual(len(cases), total_cases-1) # unit + + cases = coramin.third_party.filter_minlplib_instances(instancedata_filename=os.path.join(current_dir, 'minlplib', 'instancedata.csv'), + max_nlaghessiandiagnz=100000) + self.assertEqual(len(cases), total_cases-1) # unit + + cases = coramin.third_party.filter_minlplib_instances(instancedata_filename=os.path.join(current_dir, 'minlplib', 'instancedata.csv'), + min_nnlsemi=1) + self.assertEqual(len(cases), 0) # unit + + cases = coramin.third_party.filter_minlplib_instances(instancedata_filename=os.path.join(current_dir, 'minlplib', 'instancedata.csv'), + acceptable_objcurvature=['linear', 'convex']) + self.assertEqual(len(cases), 1220) # unit + + os.remove(os.path.join(current_dir, 'minlplib', 'instancedata.csv')) + os.rmdir(os.path.join(current_dir, 'minlplib')) + + def test_get_minlplib(self): + current_dir = this_file_dir() + coramin.third_party.get_minlplib(download_dir=os.path.join(current_dir, 'minlplib', 'osil')) + files = os.listdir(os.path.join(current_dir, 'minlplib', 'osil')) + self.assertEqual(len(files), 1594) + for i in files: + self.assertTrue(i.endswith('.osil')) + for i in os.listdir(os.path.join(current_dir, 'minlplib', 'osil')): + os.remove(os.path.join(current_dir, 'minlplib', 'osil', i)) + os.rmdir(os.path.join(current_dir, 'minlplib', 'osil')) + os.rmdir(os.path.join(current_dir, 'minlplib')) + + def test_get_minlplib_problem(self): + current_dir = os.getcwd() + coramin.third_party.get_minlplib(format='gms', problem_name='ex4_1_1') + files = os.listdir(os.path.join(current_dir, 'minlplib', 'gms')) + self.assertEqual(len(files), 1) + self.assertEqual(files[0], 'ex4_1_1.gms') + os.remove(os.path.join(current_dir, 'minlplib', 'gms', files[0])) + os.rmdir(os.path.join(current_dir, 'minlplib', 'gms')) + os.rmdir(os.path.join(current_dir, 'minlplib')) + + +class TestExceptions(unittest.TestCase): + def test_exceptions1(self): + current_dir = this_file_dir() + filename = os.path.join(current_dir, 'instancedata.csv') + f = open(filename, 'w') + f.write('blah') + f.close() + + with self.assertRaises(ValueError): + coramin.third_party.get_minlplib_instancedata(target_filename=filename) + + f = open(filename, 'r') + self.assertEqual(f.read(), 'blah') + os.remove(filename) + + def test_exceptions2(self): + current_dir = this_file_dir() + filename = os.path.join(current_dir, 'minlplib', 'instancedata.csv') + coramin.third_party.get_minlplib_instancedata(target_filename=filename) + + with self.assertRaises(ValueError): + cases = coramin.third_party.filter_minlplib_instances(instancedata_filename=filename, acceptable_probtype='foo') + + with self.assertRaises(ValueError): + cases = coramin.third_party.filter_minlplib_instances(instancedata_filename=filename, acceptable_probtype=['QCQP', 'foo']) + + os.remove(filename) + os.rmdir(os.path.dirname(filename)) + + def test_exceptions3(self): + current_dir = this_file_dir() + os.makedirs(os.path.join(current_dir, 'minlplib', 'osil')) + with self.assertRaises(ValueError): + coramin.third_party.get_minlplib(download_dir=os.path.join(current_dir, 'minlplib', 'osil')) + files = os.listdir(os.path.join(current_dir, 'minlplib', 'osil')) + self.assertEqual(len(files), 0) + os.rmdir(os.path.join(current_dir, 'minlplib', 'osil')) + os.rmdir(os.path.join(current_dir, 'minlplib')) diff --git a/pyomo/contrib/coramin/utils/__init__.py b/pyomo/contrib/coramin/utils/__init__.py new file mode 100644 index 00000000000..2c4f566a830 --- /dev/null +++ b/pyomo/contrib/coramin/utils/__init__.py @@ -0,0 +1,2 @@ +from .coramin_enums import FunctionShape, RelaxationSide, Effort, EigenValueBounder +from .pyomo_utils import get_objective, simplify_expr diff --git a/pyomo/contrib/coramin/utils/coramin_enums.py b/pyomo/contrib/coramin/utils/coramin_enums.py new file mode 100644 index 00000000000..6e997631df5 --- /dev/null +++ b/pyomo/contrib/coramin/utils/coramin_enums.py @@ -0,0 +1,30 @@ +from enum import IntEnum, Enum + + +class EigenValueBounder(IntEnum): + Gershgorin = 1 + GershgorinWithSimplification = 2 + LinearProgram = 3 + Global = 4 + + +class Effort(IntEnum): + none = 0 + very_low = 1 + low = 2 + medium = 3 + high = 4 + very_high = 5 + + +class RelaxationSide(IntEnum): + UNDER = 1 + OVER = 2 + BOTH = 3 + + +class FunctionShape(IntEnum): + LINEAR = 1 + CONVEX = 2 + CONCAVE = 3 + UNKNOWN = 4 diff --git a/pyomo/contrib/coramin/utils/mpi_utils.py b/pyomo/contrib/coramin/utils/mpi_utils.py new file mode 100644 index 00000000000..f0e9c98fae6 --- /dev/null +++ b/pyomo/contrib/coramin/utils/mpi_utils.py @@ -0,0 +1,111 @@ +from mpi4py import MPI +import numpy as np +import sys +import os + + +class MPISyncError(Exception): + pass + + +class MPIInterface: + def __init__(self): + self._comm = MPI.COMM_WORLD + self._size = self._comm.Get_size() + self._rank = self._comm.Get_rank() + + @property + def comm(self): + return self._comm + + @property + def rank(self): + return self._rank + + @property + def size(self): + return self._size + + +class MPIAllocationMap: + def __init__(self, mpi_interface, global_N): + self._mpi_interface = mpi_interface + self._global_N = global_N + + rank = self._mpi_interface.rank + size = self._mpi_interface.size + + # there must be a better way to do this + # find which entries in global correspond + # to this process (want them to be contiguous + # for the MPI Allgather calls later + local_N = [0 for i in range(self._mpi_interface.size)] + for i in range(global_N): + process_i = i % size + local_N[process_i] += 1 + + start = 0 + end = None + for i,v in enumerate(local_N): + if i == self._mpi_interface.rank: + end = start + v + break + else: + start += v + + self._local_map = list(range(start, end)) + + def local_allocation_map(self): + return list(self._local_map) + + def local_list(self, global_data): + local_data = list() + assert(len(global_data) == self._global_N) + for i in self._local_map: + local_data.append(global_data[i]) + return local_data + + def global_list_float64(self, local_data_float64): + assert(len(local_data_float64) == len(self._local_map)) + global_data_numpy = np.zeros(self._global_N, dtype='d')*np.nan + local_data_numpy = np.asarray(local_data_float64, dtype='d') + comm = self._mpi_interface.comm + comm.Allgatherv([local_data_numpy, MPI.DOUBLE], + [global_data_numpy, MPI.DOUBLE]) + + return global_data_numpy.tolist() + + +def activate_mpi_printing(style='rank-0-console', rank_0_filename='output_rank_0.txt'): + """ + Redirect standard output based on process rank. + + Parameters + ---------- + style: str + Can be set to one of: + * 'ignore-all': ignore all printing (actually, redirect all printing to os.devnull) + * 'rank-0-console': printing from rank 0 will go to the console, + printing from other processes will be ignored + * 'rank-0-console-x-files': printing from rank 0 will go to the console, + printing from other processes will go to a separate file ('output_rank_x.txt') + * 'rank-0-file': printing from rank 0 will go to 'output_rank_0.txt' + * 'separate-files': printing from each processor will be redirected to a separate + file for each process ('output_rank_x.txt') + """ + rank = MPIInterface().rank + if style == 'ignore-all': + sys.stdout = open(os.devnull, 'w') + elif style == 'rank-0-console': + if rank != 0: + sys.stdout = open(os.devnull, 'w') + elif style == 'rank-0-file': + if rank == 0: + sys.stdout = open(rank_0_filename, 'w') + else: + sys.stdout = open(os.devnull, 'w') + elif style == 'rank-0-console-x-files': + if rank != 0: + sys.stdout = open('output_rank_{0}.txt'.format(str(MPIInterface().rank)), 'w') + elif style == 'separate-files': + sys.stdout = open('output_rank_{0}.txt'.format(str(MPIInterface().rank)), 'w') diff --git a/pyomo/contrib/coramin/utils/plot_relaxation.py b/pyomo/contrib/coramin/utils/plot_relaxation.py new file mode 100644 index 00000000000..04d17754866 --- /dev/null +++ b/pyomo/contrib/coramin/utils/plot_relaxation.py @@ -0,0 +1,197 @@ +import numpy as np +try: + import plotly.graph_objects as go +except ImportError: + pass +import pyomo.environ as pe +from .pyomo_utils import get_objective +from .coramin_enums import RelaxationSide +from pyomo.solvers.plugins.solvers.persistent_solver import PersistentSolver +try: + import tqdm +except ImportError: + tqdm = None + + +def _solve(m, using_persistent_solver, solver, rhs_vars, aux_var, obj): + obj.activate() + if using_persistent_solver: + for v in rhs_vars: + solver.update_var(v) + solver.set_objective(obj) + res = solver.solve(load_solutions=False, save_results=False) + else: + res = solver.solve(m, load_solutions=False) + if res.solver.termination_condition != pe.TerminationCondition.optimal: + raise RuntimeError('Could not produce plot because solver did not terminate optimally') + if using_persistent_solver: + solver.load_vars([aux_var]) + else: + m.solutions.load_from(res) + obj.deactivate() + + +def _solve_loop(m, x, w, x_list, using_persistent_solver, solver): + w_list = list() + for _xval in x_list: + x.fix(_xval) + if using_persistent_solver: + solver.update_var(x) + res = solver.solve(load_solutions=False, save_results=False) + else: + res = solver.solve(m, load_solutions=False) + if res.solver.termination_condition != pe.TerminationCondition.optimal: + raise RuntimeError( + 'Could not produce plot because solver did not terminate optimally. Termination condition: ' + str( + res.solver.termination_condition)) + if using_persistent_solver: + solver.load_vars([w]) + else: + m.solutions.load_from(res) + w_list.append(w.value) + return w_list + + +def _plot_2d(m, relaxation, solver, show_plot, num_pts): + using_persistent_solver = isinstance(solver, PersistentSolver) + + x = relaxation.get_rhs_vars()[0] + w = relaxation.get_aux_var() + + if not x.has_lb() or not x.has_ub(): + raise ValueError('rhs var must have bounds') + + orig_xval = x.value + orig_wval = w.value + xlb = pe.value(x.lb) + xub = pe.value(x.ub) + + orig_obj = get_objective(m) + if orig_obj is not None: + orig_obj.deactivate() + + x_list = np.linspace(xlb, xub, num_pts) + x_list = [float(i) for i in x_list] + w_true = list() + + rhs_expr = relaxation.get_rhs_expr() + for _x in x_list: + x.value = float(_x) + w_true.append(pe.value(rhs_expr)) + plotly_data = [go.Scatter(x=x_list, y=w_true, name=str(rhs_expr))] + + m._plotting_objective = pe.Objective(expr=w) + if using_persistent_solver: + solver.set_instance(m) + + if relaxation.relaxation_side in {RelaxationSide.UNDER, RelaxationSide.BOTH}: + w_min = _solve_loop(m, x, w, x_list, using_persistent_solver, solver) + plotly_data.append(go.Scatter(x=x_list, y=w_min, name='underestimator')) + + del m._plotting_objective + m._plotting_objective = pe.Objective(expr=w, sense=pe.maximize) + if using_persistent_solver: + solver.set_objective(m._plotting_objective) + + if relaxation.relaxation_side in {RelaxationSide.OVER, RelaxationSide.BOTH}: + w_max = _solve_loop(m, x, w, x_list, using_persistent_solver, solver) + plotly_data.append(go.Scatter(x=x_list, y=w_max, name='overestimator')) + + fig = go.Figure(data=plotly_data) + if show_plot: + fig.show() + + x.unfix() + x.value = orig_xval + w.value = orig_wval + del m._plotting_objective + if orig_obj is not None: + orig_obj.activate() + + +def _plot_3d(m, relaxation, solver, show_plot, num_pts): + using_persistent_solver = isinstance(solver, PersistentSolver) + + rhs_vars = relaxation.get_rhs_vars() + x, y = rhs_vars + w = relaxation.get_aux_var() + + if not x.has_lb() or not x.has_ub() or not y.has_lb() or not y.has_ub(): + raise ValueError('rhs vars must have bounds') + + orig_xval = x.value + orig_yval = y.value + orig_wval = w.value + + orig_obj = get_objective(m) + if orig_obj is not None: + orig_obj.deactivate() + + m._underestimator_obj = pe.Objective(expr=w) + m._overestimator_obj = pe.Objective(expr=w, sense=pe.maximize) + m._underestimator_obj.deactivate() + m._overestimator_obj.deactivate() + if using_persistent_solver: + solver.set_instance(m) + + x_list = np.linspace(x.lb, x.ub, num_pts) + y_list = np.linspace(y.lb, y.ub, num_pts) + x_list = [float(i) for i in x_list] + y_list = [float(i) for i in y_list] + w_true = np.empty((num_pts, num_pts), dtype=np.double) + w_min = np.empty((num_pts, num_pts), dtype=np.double) + w_max = np.empty((num_pts, num_pts), dtype=np.double) + + rhs_expr = relaxation.get_rhs_expr() + + def sub_loop(x_ndx, _x): + x.fix(_x) + for y_ndx, _y in enumerate(y_list): + y.fix(_y) + w_true[x_ndx, y_ndx] = pe.value(rhs_expr) + if relaxation.relaxation_side in {RelaxationSide.UNDER, RelaxationSide.BOTH}: + _solve(m, using_persistent_solver, solver, rhs_vars, w, m._underestimator_obj) + w_min[x_ndx, y_ndx] = w.value + if relaxation.relaxation_side in {RelaxationSide.OVER, RelaxationSide.BOTH}: + _solve(m, using_persistent_solver, solver, rhs_vars, w, m._overestimator_obj) + w_max[x_ndx, y_ndx] = w.value + + if tqdm is not None: + for x_ndx, _x in tqdm.tqdm(list(enumerate(x_list))): + sub_loop(x_ndx, _x) + else: + for x_ndx, _x in enumerate(x_list): + sub_loop(x_ndx, _x) + + plotly_data = list() + plotly_data.append(go.Surface(x=x_list, y=y_list, z=w_true, name=str(rhs_expr))) + if relaxation.relaxation_side in {RelaxationSide.UNDER, RelaxationSide.BOTH}: + plotly_data.append(go.Surface(x=x_list, y=y_list, z=w_min, name='underestimator')) + if relaxation.relaxation_side in {RelaxationSide.OVER, RelaxationSide.BOTH}: + plotly_data.append(go.Surface(x=x_list, y=y_list, z=w_max, name='overestimator')) + + fig = go.Figure(data=plotly_data) + if show_plot: + fig.show() + + x.unfix() + y.unfix() + x.value = orig_xval + y.value = orig_yval + w.value = orig_wval + del m._underestimator_obj + del m._overestimator_obj + if orig_obj is not None: + orig_obj.activate() + + +def plot_relaxation(m, relaxation, solver, show_plot=True, num_pts=100): + rhs_vars = relaxation.get_rhs_vars() + + if len(rhs_vars) == 1: + _plot_2d(m, relaxation, solver, show_plot, num_pts) + elif len(rhs_vars) == 2: + _plot_3d(m, relaxation, solver, show_plot, num_pts) + else: + raise NotImplementedError('Cannot generate plot for relaxation with more than 2 RHS vars') + diff --git a/pyomo/contrib/coramin/utils/pyomo_utils.py b/pyomo/contrib/coramin/utils/pyomo_utils.py new file mode 100644 index 00000000000..6ea979cb4c7 --- /dev/null +++ b/pyomo/contrib/coramin/utils/pyomo_utils.py @@ -0,0 +1,57 @@ +import pyomo.environ as pe +from pyomo.core.expr.sympy_tools import sympyify_expression, sympy2pyomo_expression +from pyomo.core.expr.numvalue import is_fixed +from pyomo.common.collections import ComponentSet +from pyomo.core.expr.visitor import identify_variables + + +def get_objective(m): + """ + Assert that there is only one active objective in m and return it. + + Parameters + ---------- + m: pyomo.core.base.block._BlockData + + Returns + ------- + obj: pyomo.core.base.objective._ObjectiveData + """ + obj = None + for o in m.component_data_objects(pe.Objective, descend_into=True, active=True, sort=True): + if obj is not None: + raise ValueError('Found multiple active objectives') + obj = o + return obj + + +def unfixed_vars(m, descend_into=True, active=True): + for v in m.component_data_objects(pe.Var, descend_into=descend_into, active=active): + if not v.is_fixed(): + yield v + + +def active_vars(m, include_fixed=False): + seen = set() + for c in m.component_data_objects(pe.Constraint, active=True, descend_into=True): + for v in identify_variables(c.body, include_fixed=include_fixed): + v_id = id(v) + if v_id not in seen: + seen.add(v_id) + yield v + obj = get_objective(m) + if obj is not None: + for v in identify_variables(obj.expr, include_fixed=include_fixed): + v_id = id(v) + if v_id not in seen: + seen.add(v_id) + yield v + + +def simplify_expr(expr): + om, se = sympyify_expression(expr) + se = se.simplify() + new_expr = sympy2pyomo_expression(se, om) + if is_fixed(new_expr): + new_expr = pe.value(new_expr) + return new_expr From 6bf34d57933ecd1a3f176a8542bc73f04f8c9855 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Fri, 27 Oct 2023 14:20:40 -0600 Subject: [PATCH 002/128] update imports --- pyomo/contrib/coramin/__init__.py | 10 +++++----- pyomo/contrib/coramin/algorithms/ecp_bounder.py | 6 +++--- .../coramin/algorithms/multitree/multitree.py | 16 ++++++++-------- .../algorithms/multitree/tests/test_multitree.py | 2 +- .../coramin/algorithms/tests/test_ecp_bounder.py | 2 +- pyomo/contrib/coramin/domain_reduction/dbt.py | 12 ++++++------ .../contrib/coramin/domain_reduction/filters.py | 2 +- .../coramin/domain_reduction/tests/test_dbt.py | 4 ++-- pyomo/contrib/coramin/examples/alpha_bb.py | 2 +- pyomo/contrib/coramin/examples/dbt2.py | 2 +- pyomo/contrib/coramin/relaxations/alphabb.py | 8 ++++---- pyomo/contrib/coramin/relaxations/auto_relax.py | 6 +++--- .../coramin/relaxations/copy_relaxation.py | 2 +- pyomo/contrib/coramin/relaxations/hessian.py | 4 ++-- pyomo/contrib/coramin/relaxations/mccormick.py | 2 +- .../contrib/coramin/relaxations/multivariate.py | 8 ++++---- .../coramin/relaxations/relaxations_base.py | 2 +- .../coramin/relaxations/tests/test_alphabb.py | 2 +- .../coramin/relaxations/tests/test_auto_relax.py | 2 +- .../coramin/relaxations/tests/test_copy.py | 2 +- .../tests/test_univariate_relaxations.py | 2 +- pyomo/contrib/coramin/relaxations/univariate.py | 2 +- 22 files changed, 50 insertions(+), 50 deletions(-) diff --git a/pyomo/contrib/coramin/__init__.py b/pyomo/contrib/coramin/__init__.py index a191ae8bca8..6e08922d11f 100644 --- a/pyomo/contrib/coramin/__init__.py +++ b/pyomo/contrib/coramin/__init__.py @@ -1,8 +1,8 @@ -from coramin import utils -from coramin import domain_reduction -from coramin import relaxations -from coramin import algorithms -from coramin import third_party +from . import utils +from . import domain_reduction +from . import relaxations +from . import algorithms +from . import third_party from .utils import ( RelaxationSide, FunctionShape, diff --git a/pyomo/contrib/coramin/algorithms/ecp_bounder.py b/pyomo/contrib/coramin/algorithms/ecp_bounder.py index fc3177ed7b9..12e16b6ea03 100644 --- a/pyomo/contrib/coramin/algorithms/ecp_bounder.py +++ b/pyomo/contrib/coramin/algorithms/ecp_bounder.py @@ -1,13 +1,13 @@ from pyomo.contrib import appsi from pyomo.common.collections import ComponentSet -from coramin.utils.coramin_enums import RelaxationSide +from pyomo.contrib.coramin.utils.coramin_enums import RelaxationSide import pyomo.environ as pe import time from pyomo.common.config import ConfigValue, NonNegativeInt, NonNegativeFloat, In from pyomo.common.timing import HierarchicalTimer from pyomo.core.staleflag import StaleFlagManager -from coramin.utils import get_objective -from coramin.relaxations import relaxation_data_objects +from pyomo.contrib.coramin.utils import get_objective +from pyomo.contrib.coramin.relaxations import relaxation_data_objects from typing import Optional from typing import Sequence, Mapping, MutableMapping, Tuple, List from pyomo.core.base.var import _GeneralVarData diff --git a/pyomo/contrib/coramin/algorithms/multitree/multitree.py b/pyomo/contrib/coramin/algorithms/multitree/multitree.py index acab6d0836f..a74343c2ae0 100644 --- a/pyomo/contrib/coramin/algorithms/multitree/multitree.py +++ b/pyomo/contrib/coramin/algorithms/multitree/multitree.py @@ -1,5 +1,5 @@ import math -from coramin.relaxations.relaxations_base import ( +from pyomo.contrib.coramin.relaxations.relaxations_base import ( BaseRelaxationData, BasePWRelaxationData, ) @@ -21,22 +21,22 @@ ConfigValue, NonNegativeInt, PositiveFloat, PositiveInt, NonNegativeFloat, InEnum ) import logging -from coramin.relaxations.auto_relax import relax -from coramin.relaxations.iterators import relaxation_data_objects -from coramin.utils.coramin_enums import RelaxationSide, Effort, EigenValueBounder -from coramin.domain_reduction.dbt import push_integers, pop_integers, collect_vars_to_tighten -from coramin.domain_reduction.obbt import perform_obbt +from pyomo.contrib.coramin.relaxations.auto_relax import relax +from pyomo.contrib.coramin.relaxations.iterators import relaxation_data_objects +from pyomo.contrib.coramin.utils.coramin_enums import RelaxationSide, Effort, EigenValueBounder +from pyomo.contrib.coramin.domain_reduction.dbt import push_integers, pop_integers, collect_vars_to_tighten +from pyomo.contrib.coramin.domain_reduction.obbt import perform_obbt import time from pyomo.core.base.var import _GeneralVarData from pyomo.core.base.objective import _GeneralObjectiveData -from coramin.utils.pyomo_utils import get_objective, active_vars +from pyomo.contrib.coramin.utils.pyomo_utils import get_objective, active_vars from pyomo.common.collections.component_set import ComponentSet from pyomo.common.modeling import unique_component_name from pyomo.common.errors import InfeasibleConstraintException from pyomo.contrib.fbbt.fbbt import BoundsManager import numpy as np from pyomo.core.expr.visitor import identify_variables -from coramin.clone import clone_active_flat +from pyomo.contrib.coramin.clone import clone_active_flat logger = logging.getLogger(__name__) diff --git a/pyomo/contrib/coramin/algorithms/multitree/tests/test_multitree.py b/pyomo/contrib/coramin/algorithms/multitree/tests/test_multitree.py index dfcb08f8e95..3acb719743a 100644 --- a/pyomo/contrib/coramin/algorithms/multitree/tests/test_multitree.py +++ b/pyomo/contrib/coramin/algorithms/multitree/tests/test_multitree.py @@ -1,7 +1,7 @@ import math import coramin -from coramin.third_party.minlplib_tools import get_minlplib, get_minlplib_instancedata +from pyomo.contrib.coramin.third_party.minlplib_tools import get_minlplib, get_minlplib_instancedata import unittest from pyomo.contrib import appsi import os diff --git a/pyomo/contrib/coramin/algorithms/tests/test_ecp_bounder.py b/pyomo/contrib/coramin/algorithms/tests/test_ecp_bounder.py index f99dc9f2003..2dc20a361a3 100644 --- a/pyomo/contrib/coramin/algorithms/tests/test_ecp_bounder.py +++ b/pyomo/contrib/coramin/algorithms/tests/test_ecp_bounder.py @@ -1,6 +1,6 @@ import pyomo.environ as pe import coramin -from coramin.algorithms.ecp_bounder import ECPBounder +from pyomo.contrib.coramin.algorithms.ecp_bounder import ECPBounder import unittest import logging from pyomo.contrib import appsi diff --git a/pyomo/contrib/coramin/domain_reduction/dbt.py b/pyomo/contrib/coramin/domain_reduction/dbt.py index c5c9530a525..2b659431f1a 100644 --- a/pyomo/contrib/coramin/domain_reduction/dbt.py +++ b/pyomo/contrib/coramin/domain_reduction/dbt.py @@ -11,7 +11,7 @@ from pyomo.core.expr.visitor import identify_variables from pyomo.common.collections import ComponentSet from pyomo.common.collections.orderedset import OrderedSet -from coramin.relaxations.iterators import relaxation_data_objects, nonrelaxation_component_data_objects +from pyomo.contrib.coramin.relaxations.iterators import relaxation_data_objects, nonrelaxation_component_data_objects from pyomo.core.expr.visitor import replace_expressions import logging import networkx @@ -23,16 +23,16 @@ import numpy as np import math from pyomo.core.base.block import declare_custom_block, _BlockData -from coramin.utils.pyomo_utils import get_objective +from pyomo.contrib.coramin.utils.pyomo_utils import get_objective from pyomo.core.base.var import _GeneralVarData -from coramin.relaxations.copy_relaxation import copy_relaxation_with_local_data -from coramin.relaxations.relaxations_base import BaseRelaxationData -from coramin.utils import RelaxationSide +from pyomo.contrib.coramin.relaxations.copy_relaxation import copy_relaxation_with_local_data +from pyomo.contrib.coramin.relaxations.relaxations_base import BaseRelaxationData +from pyomo.contrib.coramin.utils import RelaxationSide from collections import defaultdict from pyomo.core.expr import numeric_expr from pyomo.core.expr.visitor import ExpressionValueVisitor, nonpyomo_leaf_types from pyomo.common.modeling import unique_component_name -from coramin.relaxations.split_expr import flatten_expr +from pyomo.contrib.coramin.relaxations.split_expr import flatten_expr logger = logging.getLogger(__name__) diff --git a/pyomo/contrib/coramin/domain_reduction/filters.py b/pyomo/contrib/coramin/domain_reduction/filters.py index d0fd3ccb10a..5cf0822ce89 100644 --- a/pyomo/contrib/coramin/domain_reduction/filters.py +++ b/pyomo/contrib/coramin/domain_reduction/filters.py @@ -1,5 +1,5 @@ from pyomo.common.collections import ComponentSet -from coramin.domain_reduction.obbt import _bt_prep, _bt_cleanup +from pyomo.contrib.coramin.domain_reduction.obbt import _bt_prep, _bt_cleanup import pyomo.environ as pe from pyomo.core.expr.numeric_expr import LinearExpression import logging diff --git a/pyomo/contrib/coramin/domain_reduction/tests/test_dbt.py b/pyomo/contrib/coramin/domain_reduction/tests/test_dbt.py index 329139cea9d..f757d56a82c 100644 --- a/pyomo/contrib/coramin/domain_reduction/tests/test_dbt.py +++ b/pyomo/contrib/coramin/domain_reduction/tests/test_dbt.py @@ -1,4 +1,4 @@ -from coramin.domain_reduction.dbt import TreeBlock, TreeBlockError, convert_pyomo_model_to_bipartite_graph, \ +from pyomo.contrib.coramin.domain_reduction.dbt import TreeBlock, TreeBlockError, convert_pyomo_model_to_bipartite_graph, \ _VarNode, _ConNode, _RelNode, split_metis, num_cons_in_graph, collect_vars_to_tighten_by_block, decompose_model, \ perform_dbt, OBBTMethod, FilterMethod import unittest @@ -13,7 +13,7 @@ from egret.data.model_data import ModelData from egret.models.acopf import create_psv_acopf_model import os -from coramin.utils.pyomo_utils import get_objective +from pyomo.contrib.coramin.utils.pyomo_utils import get_objective import filecmp from pyomo.contrib import appsi import pytest diff --git a/pyomo/contrib/coramin/examples/alpha_bb.py b/pyomo/contrib/coramin/examples/alpha_bb.py index af4d6c36ef3..451deb5ed86 100644 --- a/pyomo/contrib/coramin/examples/alpha_bb.py +++ b/pyomo/contrib/coramin/examples/alpha_bb.py @@ -1,6 +1,6 @@ import pyomo.environ as pe import coramin -from coramin.utils.plot_relaxation import plot_relaxation +from pyomo.contrib.coramin.utils.plot_relaxation import plot_relaxation from pyomo.contrib import appsi diff --git a/pyomo/contrib/coramin/examples/dbt2.py b/pyomo/contrib/coramin/examples/dbt2.py index 8864997b5e7..6d82ad985ef 100644 --- a/pyomo/contrib/coramin/examples/dbt2.py +++ b/pyomo/contrib/coramin/examples/dbt2.py @@ -11,7 +11,7 @@ import os import time from suspect.pyomo import read_osil -from coramin.third_party.minlplib_tools import get_minlplib +from pyomo.contrib.coramin.third_party.minlplib_tools import get_minlplib print('Downloading camshape800 from MINLPLib') diff --git a/pyomo/contrib/coramin/relaxations/alphabb.py b/pyomo/contrib/coramin/relaxations/alphabb.py index e10b7d1320c..2b88da6a05b 100644 --- a/pyomo/contrib/coramin/relaxations/alphabb.py +++ b/pyomo/contrib/coramin/relaxations/alphabb.py @@ -1,7 +1,7 @@ -from coramin.utils.coramin_enums import EigenValueBounder, RelaxationSide -from coramin.relaxations.custom_block import declare_custom_block -from coramin.relaxations.relaxations_base import BaseRelaxationData, ComponentWeakRef -from coramin.relaxations.hessian import Hessian +from pyomo.contrib.coramin.utils.coramin_enums import EigenValueBounder, RelaxationSide +from pyomo.contrib.coramin.relaxations.custom_block import declare_custom_block +from pyomo.contrib.coramin.relaxations.relaxations_base import BaseRelaxationData, ComponentWeakRef +from pyomo.contrib.coramin.relaxations.hessian import Hessian from typing import Optional, Tuple from pyomo.core.base.var import _GeneralVarData from pyomo.core.expr.numeric_expr import ExpressionBase diff --git a/pyomo/contrib/coramin/relaxations/auto_relax.py b/pyomo/contrib/coramin/relaxations/auto_relax.py index 262a7d67471..751d048a8ff 100644 --- a/pyomo/contrib/coramin/relaxations/auto_relax.py +++ b/pyomo/contrib/coramin/relaxations/auto_relax.py @@ -16,16 +16,16 @@ from .mccormick import PWMcCormickRelaxation from .multivariate import MultivariateRelaxation from .alphabb import AlphaBBRelaxation -from coramin.utils.coramin_enums import RelaxationSide, FunctionShape, Effort, EigenValueBounder +from pyomo.contrib.coramin.utils.coramin_enums import RelaxationSide, FunctionShape, Effort, EigenValueBounder from pyomo.gdp import Disjunct from pyomo.core.base.expression import _GeneralExpressionData, SimpleExpression -from coramin.relaxations.iterators import nonrelaxation_component_data_objects +from pyomo.contrib.coramin.relaxations.iterators import nonrelaxation_component_data_objects from pyomo.contrib import appsi from pyomo.repn.standard_repn import generate_standard_repn from pyomo.contrib.fbbt import interval from pyomo.core.expr.compare import convert_expression_to_prefix_notation from .split_expr import split_expr -from coramin.utils.pyomo_utils import simplify_expr, active_vars +from pyomo.contrib.coramin.utils.pyomo_utils import simplify_expr, active_vars from .hessian import Hessian from typing import MutableMapping, Tuple, Union, Optional from pyomo.core.base.block import _BlockData diff --git a/pyomo/contrib/coramin/relaxations/copy_relaxation.py b/pyomo/contrib/coramin/relaxations/copy_relaxation.py index 10b1b83e06f..832eabc8691 100644 --- a/pyomo/contrib/coramin/relaxations/copy_relaxation.py +++ b/pyomo/contrib/coramin/relaxations/copy_relaxation.py @@ -6,7 +6,7 @@ from .alphabb import AlphaBBRelaxation, AlphaBBRelaxationData from .multivariate import MultivariateRelaxationData, MultivariateRelaxation from pyomo.core.expr.visitor import replace_expressions -from coramin.utils.coramin_enums import FunctionShape +from pyomo.contrib.coramin.utils.coramin_enums import FunctionShape def copy_relaxation_with_local_data(rel, old_var_to_new_var_map): diff --git a/pyomo/contrib/coramin/relaxations/hessian.py b/pyomo/contrib/coramin/relaxations/hessian.py index 02d0f469de5..3e0a98668da 100644 --- a/pyomo/contrib/coramin/relaxations/hessian.py +++ b/pyomo/contrib/coramin/relaxations/hessian.py @@ -7,14 +7,14 @@ import math from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr import numpy as np -from coramin.utils.coramin_enums import EigenValueBounder +from pyomo.contrib.coramin.utils.coramin_enums import EigenValueBounder from pyomo.core.base.block import _BlockData from typing import Optional, MutableMapping from pyomo.core.expr.numeric_expr import ExpressionBase from pyomo.contrib import appsi from pyomo.common.modeling import unique_component_name from pyomo.core.base.var import _GeneralVarData -from coramin.utils.pyomo_utils import simplify_expr +from pyomo.contrib.coramin.utils.pyomo_utils import simplify_expr def _2d_determinant(mat: np.ndarray): diff --git a/pyomo/contrib/coramin/relaxations/mccormick.py b/pyomo/contrib/coramin/relaxations/mccormick.py index 3b5aea907cb..fc82dbefb91 100644 --- a/pyomo/contrib/coramin/relaxations/mccormick.py +++ b/pyomo/contrib/coramin/relaxations/mccormick.py @@ -1,6 +1,6 @@ import logging import pyomo.environ as pyo -from coramin.utils.coramin_enums import RelaxationSide +from pyomo.contrib.coramin.utils.coramin_enums import RelaxationSide from .custom_block import declare_custom_block from .relaxations_base import BasePWRelaxationData, ComponentWeakRef, _check_cut import math diff --git a/pyomo/contrib/coramin/relaxations/multivariate.py b/pyomo/contrib/coramin/relaxations/multivariate.py index 895a464a4a5..015f173c4ff 100644 --- a/pyomo/contrib/coramin/relaxations/multivariate.py +++ b/pyomo/contrib/coramin/relaxations/multivariate.py @@ -1,10 +1,10 @@ -from coramin.utils.coramin_enums import RelaxationSide, FunctionShape -from coramin.relaxations.custom_block import declare_custom_block -from coramin.relaxations.relaxations_base import BaseRelaxationData, ComponentWeakRef +from pyomo.contrib.coramin.utils.coramin_enums import RelaxationSide, FunctionShape +from pyomo.contrib.coramin.relaxations.custom_block import declare_custom_block +from pyomo.contrib.coramin.relaxations.relaxations_base import BaseRelaxationData, ComponentWeakRef from pyomo.core.expr.visitor import identify_variables import math import pyomo.environ as pe -from coramin.relaxations._utils import _get_bnds_list +from pyomo.contrib.coramin.relaxations._utils import _get_bnds_list @declare_custom_block(name='MultivariateRelaxation') diff --git a/pyomo/contrib/coramin/relaxations/relaxations_base.py b/pyomo/contrib/coramin/relaxations/relaxations_base.py index da74db9cbdd..eb33f009894 100644 --- a/pyomo/contrib/coramin/relaxations/relaxations_base.py +++ b/pyomo/contrib/coramin/relaxations/relaxations_base.py @@ -4,7 +4,7 @@ import pyomo.environ as pe from collections.abc import Iterable from pyomo.common.collections import ComponentSet, ComponentMap -from coramin.utils.coramin_enums import FunctionShape, RelaxationSide +from pyomo.contrib.coramin.utils.coramin_enums import FunctionShape, RelaxationSide import warnings import logging import math diff --git a/pyomo/contrib/coramin/relaxations/tests/test_alphabb.py b/pyomo/contrib/coramin/relaxations/tests/test_alphabb.py index 08b06c88428..51f8ad4e721 100644 --- a/pyomo/contrib/coramin/relaxations/tests/test_alphabb.py +++ b/pyomo/contrib/coramin/relaxations/tests/test_alphabb.py @@ -3,7 +3,7 @@ import math import pyomo.environ as pe import coramin -from coramin.relaxations.alphabb import AlphaBBRelaxation +from pyomo.contrib.coramin.relaxations.alphabb import AlphaBBRelaxation class TestAlphaBBRelaxation(unittest.TestCase): diff --git a/pyomo/contrib/coramin/relaxations/tests/test_auto_relax.py b/pyomo/contrib/coramin/relaxations/tests/test_auto_relax.py index e2f2a56fb41..708765eb215 100644 --- a/pyomo/contrib/coramin/relaxations/tests/test_auto_relax.py +++ b/pyomo/contrib/coramin/relaxations/tests/test_auto_relax.py @@ -9,7 +9,7 @@ from pyomo.core.base.param import _ParamData, ScalarParam from pyomo.core.expr.sympy_tools import sympyify_expression from pyomo.contrib import appsi -from coramin.utils import RelaxationSide, Effort, EigenValueBounder +from pyomo.contrib.coramin.utils import RelaxationSide, Effort, EigenValueBounder class TestAutoRelax(unittest.TestCase): diff --git a/pyomo/contrib/coramin/relaxations/tests/test_copy.py b/pyomo/contrib/coramin/relaxations/tests/test_copy.py index a78c73555b5..92c8c4696d9 100644 --- a/pyomo/contrib/coramin/relaxations/tests/test_copy.py +++ b/pyomo/contrib/coramin/relaxations/tests/test_copy.py @@ -1,4 +1,4 @@ -from coramin.relaxations.copy_relaxation import copy_relaxation_with_local_data +from pyomo.contrib.coramin.relaxations.copy_relaxation import copy_relaxation_with_local_data import unittest import pyomo.environ as pe import coramin diff --git a/pyomo/contrib/coramin/relaxations/tests/test_univariate_relaxations.py b/pyomo/contrib/coramin/relaxations/tests/test_univariate_relaxations.py index a7c2053534a..b23690a0b55 100644 --- a/pyomo/contrib/coramin/relaxations/tests/test_univariate_relaxations.py +++ b/pyomo/contrib/coramin/relaxations/tests/test_univariate_relaxations.py @@ -3,7 +3,7 @@ import pyomo.environ as pe import coramin import numpy as np -from coramin.relaxations.segments import compute_k_segment_points +from pyomo.contrib.coramin.relaxations.segments import compute_k_segment_points class TestUnivariateExp(unittest.TestCase): diff --git a/pyomo/contrib/coramin/relaxations/univariate.py b/pyomo/contrib/coramin/relaxations/univariate.py index 0025cf1490c..1bef9a7b132 100644 --- a/pyomo/contrib/coramin/relaxations/univariate.py +++ b/pyomo/contrib/coramin/relaxations/univariate.py @@ -1,5 +1,5 @@ import pyomo.environ as pyo -from coramin.utils.coramin_enums import RelaxationSide, FunctionShape +from pyomo.contrib.coramin.utils.coramin_enums import RelaxationSide, FunctionShape from .relaxations_base import BasePWRelaxationData, ComponentWeakRef, _check_cut from .custom_block import declare_custom_block import numpy as np From 3e8b60fa54fb687ed6efd85e5c6c26226ba47cb9 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Fri, 27 Oct 2023 14:37:05 -0600 Subject: [PATCH 003/128] update imports --- .../coramin/algorithms/multitree/tests/test_multitree.py | 2 +- pyomo/contrib/coramin/algorithms/tests/test_ecp_bounder.py | 2 +- pyomo/contrib/coramin/domain_reduction/tests/test_dbt.py | 2 +- pyomo/contrib/coramin/domain_reduction/tests/test_filters.py | 2 +- pyomo/contrib/coramin/domain_reduction/tests/test_obbt.py | 2 +- pyomo/contrib/coramin/relaxations/tests/test_alphabb.py | 2 +- pyomo/contrib/coramin/relaxations/tests/test_auto_relax.py | 2 +- pyomo/contrib/coramin/relaxations/tests/test_copy.py | 2 +- pyomo/contrib/coramin/relaxations/tests/test_iterators.py | 2 +- pyomo/contrib/coramin/relaxations/tests/test_mccormick.py | 2 +- pyomo/contrib/coramin/relaxations/tests/test_relaxations.py | 2 +- .../contrib/coramin/relaxations/tests/test_relaxations_base.py | 2 +- .../coramin/relaxations/tests/test_univariate_relaxations.py | 2 +- pyomo/contrib/coramin/third_party/tests/test_minlplib_tools.py | 2 +- 14 files changed, 14 insertions(+), 14 deletions(-) diff --git a/pyomo/contrib/coramin/algorithms/multitree/tests/test_multitree.py b/pyomo/contrib/coramin/algorithms/multitree/tests/test_multitree.py index 3acb719743a..67a2eeed47f 100644 --- a/pyomo/contrib/coramin/algorithms/multitree/tests/test_multitree.py +++ b/pyomo/contrib/coramin/algorithms/multitree/tests/test_multitree.py @@ -2,7 +2,7 @@ import coramin from pyomo.contrib.coramin.third_party.minlplib_tools import get_minlplib, get_minlplib_instancedata -import unittest +from pyomo.common import unittest from pyomo.contrib import appsi import os import logging diff --git a/pyomo/contrib/coramin/algorithms/tests/test_ecp_bounder.py b/pyomo/contrib/coramin/algorithms/tests/test_ecp_bounder.py index 2dc20a361a3..9a93d387d1b 100644 --- a/pyomo/contrib/coramin/algorithms/tests/test_ecp_bounder.py +++ b/pyomo/contrib/coramin/algorithms/tests/test_ecp_bounder.py @@ -1,7 +1,7 @@ import pyomo.environ as pe import coramin from pyomo.contrib.coramin.algorithms.ecp_bounder import ECPBounder -import unittest +from pyomo.common import unittest import logging from pyomo.contrib import appsi diff --git a/pyomo/contrib/coramin/domain_reduction/tests/test_dbt.py b/pyomo/contrib/coramin/domain_reduction/tests/test_dbt.py index f757d56a82c..982031a96d3 100644 --- a/pyomo/contrib/coramin/domain_reduction/tests/test_dbt.py +++ b/pyomo/contrib/coramin/domain_reduction/tests/test_dbt.py @@ -1,7 +1,7 @@ from pyomo.contrib.coramin.domain_reduction.dbt import TreeBlock, TreeBlockError, convert_pyomo_model_to_bipartite_graph, \ _VarNode, _ConNode, _RelNode, split_metis, num_cons_in_graph, collect_vars_to_tighten_by_block, decompose_model, \ perform_dbt, OBBTMethod, FilterMethod -import unittest +from pyomo.common import unittest import pyomo.environ as pe import coramin from networkx import is_bipartite diff --git a/pyomo/contrib/coramin/domain_reduction/tests/test_filters.py b/pyomo/contrib/coramin/domain_reduction/tests/test_filters.py index e15dd10be98..236fb02efa5 100644 --- a/pyomo/contrib/coramin/domain_reduction/tests/test_filters.py +++ b/pyomo/contrib/coramin/domain_reduction/tests/test_filters.py @@ -1,6 +1,6 @@ import pyomo.environ as pe import coramin -import unittest +from pyomo.common import unittest from pyomo.contrib import appsi diff --git a/pyomo/contrib/coramin/domain_reduction/tests/test_obbt.py b/pyomo/contrib/coramin/domain_reduction/tests/test_obbt.py index 8c0a151dc85..3f2c1df8e3a 100644 --- a/pyomo/contrib/coramin/domain_reduction/tests/test_obbt.py +++ b/pyomo/contrib/coramin/domain_reduction/tests/test_obbt.py @@ -1,5 +1,5 @@ import coramin -import unittest +from pyomo.common import unittest import pyomo.environ as pyo from pyomo.contrib import appsi diff --git a/pyomo/contrib/coramin/relaxations/tests/test_alphabb.py b/pyomo/contrib/coramin/relaxations/tests/test_alphabb.py index 51f8ad4e721..39fe516d9c5 100644 --- a/pyomo/contrib/coramin/relaxations/tests/test_alphabb.py +++ b/pyomo/contrib/coramin/relaxations/tests/test_alphabb.py @@ -1,4 +1,4 @@ -import unittest +from pyomo.common import unittest import itertools import math import pyomo.environ as pe diff --git a/pyomo/contrib/coramin/relaxations/tests/test_auto_relax.py b/pyomo/contrib/coramin/relaxations/tests/test_auto_relax.py index 708765eb215..325e5def8e5 100644 --- a/pyomo/contrib/coramin/relaxations/tests/test_auto_relax.py +++ b/pyomo/contrib/coramin/relaxations/tests/test_auto_relax.py @@ -1,6 +1,6 @@ import pyomo.environ as pe import coramin -import unittest +from pyomo.common import unittest from pyomo.core.expr.calculus.diff_with_pyomo import reverse_sd from pyomo.core.expr.visitor import identify_variables, identify_components import math diff --git a/pyomo/contrib/coramin/relaxations/tests/test_copy.py b/pyomo/contrib/coramin/relaxations/tests/test_copy.py index 92c8c4696d9..5303883d74e 100644 --- a/pyomo/contrib/coramin/relaxations/tests/test_copy.py +++ b/pyomo/contrib/coramin/relaxations/tests/test_copy.py @@ -1,5 +1,5 @@ from pyomo.contrib.coramin.relaxations.copy_relaxation import copy_relaxation_with_local_data -import unittest +from pyomo.common import unittest import pyomo.environ as pe import coramin from pyomo.common.collections import ComponentSet diff --git a/pyomo/contrib/coramin/relaxations/tests/test_iterators.py b/pyomo/contrib/coramin/relaxations/tests/test_iterators.py index a1d2417c4f5..43918018e63 100644 --- a/pyomo/contrib/coramin/relaxations/tests/test_iterators.py +++ b/pyomo/contrib/coramin/relaxations/tests/test_iterators.py @@ -1,5 +1,5 @@ import coramin -import unittest +from pyomo.common import unittest import pyomo.environ as pe from pyomo.common.collections import ComponentSet diff --git a/pyomo/contrib/coramin/relaxations/tests/test_mccormick.py b/pyomo/contrib/coramin/relaxations/tests/test_mccormick.py index 053bdf80bb7..e040ed3fa9b 100644 --- a/pyomo/contrib/coramin/relaxations/tests/test_mccormick.py +++ b/pyomo/contrib/coramin/relaxations/tests/test_mccormick.py @@ -1,5 +1,5 @@ import pyomo.environ as pyo -import unittest +from pyomo.common import unittest import coramin diff --git a/pyomo/contrib/coramin/relaxations/tests/test_relaxations.py b/pyomo/contrib/coramin/relaxations/tests/test_relaxations.py index db3ad094432..1091902893d 100644 --- a/pyomo/contrib/coramin/relaxations/tests/test_relaxations.py +++ b/pyomo/contrib/coramin/relaxations/tests/test_relaxations.py @@ -1,4 +1,4 @@ -import unittest +from pyomo.common import unittest import pyomo.environ as pe import coramin from pyomo.core.base.block import _BlockData diff --git a/pyomo/contrib/coramin/relaxations/tests/test_relaxations_base.py b/pyomo/contrib/coramin/relaxations/tests/test_relaxations_base.py index 186b5128f33..69f0bff0b94 100644 --- a/pyomo/contrib/coramin/relaxations/tests/test_relaxations_base.py +++ b/pyomo/contrib/coramin/relaxations/tests/test_relaxations_base.py @@ -1,4 +1,4 @@ -import unittest +from pyomo.common import unittest import pyomo.environ as pe from pyomo.opt import assert_optimal_termination import coramin diff --git a/pyomo/contrib/coramin/relaxations/tests/test_univariate_relaxations.py b/pyomo/contrib/coramin/relaxations/tests/test_univariate_relaxations.py index b23690a0b55..5f9a532c936 100644 --- a/pyomo/contrib/coramin/relaxations/tests/test_univariate_relaxations.py +++ b/pyomo/contrib/coramin/relaxations/tests/test_univariate_relaxations.py @@ -1,4 +1,4 @@ -import unittest +from pyomo.common import unittest import math import pyomo.environ as pe import coramin diff --git a/pyomo/contrib/coramin/third_party/tests/test_minlplib_tools.py b/pyomo/contrib/coramin/third_party/tests/test_minlplib_tools.py index c5c442f1284..15b58a33e38 100644 --- a/pyomo/contrib/coramin/third_party/tests/test_minlplib_tools.py +++ b/pyomo/contrib/coramin/third_party/tests/test_minlplib_tools.py @@ -1,5 +1,5 @@ import coramin -import unittest +from pyomo.common import unittest import os from pyomo.common.fileutils import this_file_dir from urllib.request import urlopen From 0406fe222361027651cd7825106ec3d6b17014d3 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Fri, 27 Oct 2023 14:54:31 -0600 Subject: [PATCH 004/128] update imports --- .../coramin/algorithms/multitree/tests/test_multitree.py | 2 +- pyomo/contrib/coramin/algorithms/tests/test_ecp_bounder.py | 2 +- pyomo/contrib/coramin/domain_reduction/obbt.py | 2 +- pyomo/contrib/coramin/domain_reduction/tests/test_dbt.py | 2 +- pyomo/contrib/coramin/domain_reduction/tests/test_filters.py | 2 +- pyomo/contrib/coramin/domain_reduction/tests/test_obbt.py | 2 +- pyomo/contrib/coramin/examples/alpha_bb.py | 2 +- pyomo/contrib/coramin/examples/dbt.py | 2 +- pyomo/contrib/coramin/examples/dbt2.py | 2 +- pyomo/contrib/coramin/examples/ex.py | 2 +- pyomo/contrib/coramin/examples/rosenbrock.py | 2 +- pyomo/contrib/coramin/relaxations/tests/test_alphabb.py | 2 +- pyomo/contrib/coramin/relaxations/tests/test_auto_relax.py | 2 +- pyomo/contrib/coramin/relaxations/tests/test_copy.py | 2 +- pyomo/contrib/coramin/relaxations/tests/test_iterators.py | 2 +- pyomo/contrib/coramin/relaxations/tests/test_mccormick.py | 2 +- pyomo/contrib/coramin/relaxations/tests/test_relaxations.py | 2 +- .../contrib/coramin/relaxations/tests/test_relaxations_base.py | 2 +- .../coramin/relaxations/tests/test_univariate_relaxations.py | 2 +- pyomo/contrib/coramin/third_party/tests/test_minlplib_tools.py | 2 +- 20 files changed, 20 insertions(+), 20 deletions(-) diff --git a/pyomo/contrib/coramin/algorithms/multitree/tests/test_multitree.py b/pyomo/contrib/coramin/algorithms/multitree/tests/test_multitree.py index 67a2eeed47f..972a1cf4b49 100644 --- a/pyomo/contrib/coramin/algorithms/multitree/tests/test_multitree.py +++ b/pyomo/contrib/coramin/algorithms/multitree/tests/test_multitree.py @@ -1,6 +1,6 @@ import math -import coramin +from pyomo.contrib import coramin from pyomo.contrib.coramin.third_party.minlplib_tools import get_minlplib, get_minlplib_instancedata from pyomo.common import unittest from pyomo.contrib import appsi diff --git a/pyomo/contrib/coramin/algorithms/tests/test_ecp_bounder.py b/pyomo/contrib/coramin/algorithms/tests/test_ecp_bounder.py index 9a93d387d1b..8b841999549 100644 --- a/pyomo/contrib/coramin/algorithms/tests/test_ecp_bounder.py +++ b/pyomo/contrib/coramin/algorithms/tests/test_ecp_bounder.py @@ -1,5 +1,5 @@ import pyomo.environ as pe -import coramin +from pyomo.contrib import coramin from pyomo.contrib.coramin.algorithms.ecp_bounder import ECPBounder from pyomo.common import unittest import logging diff --git a/pyomo/contrib/coramin/domain_reduction/obbt.py b/pyomo/contrib/coramin/domain_reduction/obbt.py index 8b4248411cd..77abafe0f02 100644 --- a/pyomo/contrib/coramin/domain_reduction/obbt.py +++ b/pyomo/contrib/coramin/domain_reduction/obbt.py @@ -13,7 +13,7 @@ from typing import Union, Sequence, Optional, List from pyomo.core.base.objective import ScalarObjective, _GeneralObjectiveData try: - import coramin.utils.mpi_utils as mpiu + import pyomo.contrib.coramin.utils.mpi_utils as mpiu mpi_available = True except ImportError: mpi_available = False diff --git a/pyomo/contrib/coramin/domain_reduction/tests/test_dbt.py b/pyomo/contrib/coramin/domain_reduction/tests/test_dbt.py index 982031a96d3..6ed3d765484 100644 --- a/pyomo/contrib/coramin/domain_reduction/tests/test_dbt.py +++ b/pyomo/contrib/coramin/domain_reduction/tests/test_dbt.py @@ -3,7 +3,7 @@ perform_dbt, OBBTMethod, FilterMethod from pyomo.common import unittest import pyomo.environ as pe -import coramin +from pyomo.contrib import coramin from networkx import is_bipartite from pyomo.common.collections import ComponentSet from networkx import Graph diff --git a/pyomo/contrib/coramin/domain_reduction/tests/test_filters.py b/pyomo/contrib/coramin/domain_reduction/tests/test_filters.py index 236fb02efa5..8e60e90f591 100644 --- a/pyomo/contrib/coramin/domain_reduction/tests/test_filters.py +++ b/pyomo/contrib/coramin/domain_reduction/tests/test_filters.py @@ -1,5 +1,5 @@ import pyomo.environ as pe -import coramin +from pyomo.contrib import coramin from pyomo.common import unittest from pyomo.contrib import appsi diff --git a/pyomo/contrib/coramin/domain_reduction/tests/test_obbt.py b/pyomo/contrib/coramin/domain_reduction/tests/test_obbt.py index 3f2c1df8e3a..c2535c5254a 100644 --- a/pyomo/contrib/coramin/domain_reduction/tests/test_obbt.py +++ b/pyomo/contrib/coramin/domain_reduction/tests/test_obbt.py @@ -1,4 +1,4 @@ -import coramin +from pyomo.contrib import coramin from pyomo.common import unittest import pyomo.environ as pyo from pyomo.contrib import appsi diff --git a/pyomo/contrib/coramin/examples/alpha_bb.py b/pyomo/contrib/coramin/examples/alpha_bb.py index 451deb5ed86..c3d53eb6de4 100644 --- a/pyomo/contrib/coramin/examples/alpha_bb.py +++ b/pyomo/contrib/coramin/examples/alpha_bb.py @@ -1,5 +1,5 @@ import pyomo.environ as pe -import coramin +from pyomo.contrib import coramin from pyomo.contrib.coramin.utils.plot_relaxation import plot_relaxation from pyomo.contrib import appsi diff --git a/pyomo/contrib/coramin/examples/dbt.py b/pyomo/contrib/coramin/examples/dbt.py index 94146ad66eb..90d175f51ac 100644 --- a/pyomo/contrib/coramin/examples/dbt.py +++ b/pyomo/contrib/coramin/examples/dbt.py @@ -3,7 +3,7 @@ tightening. The example problem is an ACOPF problem. """ import pyomo.environ as pe -import coramin +from pyomo.contrib import coramin from egret.data.model_data import ModelData from egret.thirdparty.get_pglib_opf import get_pglib_opf from egret.models.ac_relaxations import create_polar_acopf_relaxation diff --git a/pyomo/contrib/coramin/examples/dbt2.py b/pyomo/contrib/coramin/examples/dbt2.py index 6d82ad985ef..1b2fd08214f 100644 --- a/pyomo/contrib/coramin/examples/dbt2.py +++ b/pyomo/contrib/coramin/examples/dbt2.py @@ -6,7 +6,7 @@ downloaded from minlplib.org. Suspect is also needed. """ import pyomo.environ as pe -import coramin +from pyomo.contrib import coramin import itertools import os import time diff --git a/pyomo/contrib/coramin/examples/ex.py b/pyomo/contrib/coramin/examples/ex.py index 4ec272f34c7..5f5aceff845 100644 --- a/pyomo/contrib/coramin/examples/ex.py +++ b/pyomo/contrib/coramin/examples/ex.py @@ -1,5 +1,5 @@ import pyomo.environ as pe -import coramin +from pyomo.contrib import coramin from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr diff --git a/pyomo/contrib/coramin/examples/rosenbrock.py b/pyomo/contrib/coramin/examples/rosenbrock.py index e0a58f860d6..acb1ac4cc52 100644 --- a/pyomo/contrib/coramin/examples/rosenbrock.py +++ b/pyomo/contrib/coramin/examples/rosenbrock.py @@ -1,5 +1,5 @@ import pyomo.environ as pe -import coramin +from pyomo.contrib import coramin def create_nlp(a, b): diff --git a/pyomo/contrib/coramin/relaxations/tests/test_alphabb.py b/pyomo/contrib/coramin/relaxations/tests/test_alphabb.py index 39fe516d9c5..ef9b98f8943 100644 --- a/pyomo/contrib/coramin/relaxations/tests/test_alphabb.py +++ b/pyomo/contrib/coramin/relaxations/tests/test_alphabb.py @@ -2,7 +2,7 @@ import itertools import math import pyomo.environ as pe -import coramin +from pyomo.contrib import coramin from pyomo.contrib.coramin.relaxations.alphabb import AlphaBBRelaxation diff --git a/pyomo/contrib/coramin/relaxations/tests/test_auto_relax.py b/pyomo/contrib/coramin/relaxations/tests/test_auto_relax.py index 325e5def8e5..70aecf12707 100644 --- a/pyomo/contrib/coramin/relaxations/tests/test_auto_relax.py +++ b/pyomo/contrib/coramin/relaxations/tests/test_auto_relax.py @@ -1,5 +1,5 @@ import pyomo.environ as pe -import coramin +from pyomo.contrib import coramin from pyomo.common import unittest from pyomo.core.expr.calculus.diff_with_pyomo import reverse_sd from pyomo.core.expr.visitor import identify_variables, identify_components diff --git a/pyomo/contrib/coramin/relaxations/tests/test_copy.py b/pyomo/contrib/coramin/relaxations/tests/test_copy.py index 5303883d74e..78be877093f 100644 --- a/pyomo/contrib/coramin/relaxations/tests/test_copy.py +++ b/pyomo/contrib/coramin/relaxations/tests/test_copy.py @@ -1,7 +1,7 @@ from pyomo.contrib.coramin.relaxations.copy_relaxation import copy_relaxation_with_local_data from pyomo.common import unittest import pyomo.environ as pe -import coramin +from pyomo.contrib import coramin from pyomo.common.collections import ComponentSet from pyomo.core.expr.sympy_tools import sympyify_expression diff --git a/pyomo/contrib/coramin/relaxations/tests/test_iterators.py b/pyomo/contrib/coramin/relaxations/tests/test_iterators.py index 43918018e63..c248784b0ce 100644 --- a/pyomo/contrib/coramin/relaxations/tests/test_iterators.py +++ b/pyomo/contrib/coramin/relaxations/tests/test_iterators.py @@ -1,4 +1,4 @@ -import coramin +from pyomo.contrib import coramin from pyomo.common import unittest import pyomo.environ as pe from pyomo.common.collections import ComponentSet diff --git a/pyomo/contrib/coramin/relaxations/tests/test_mccormick.py b/pyomo/contrib/coramin/relaxations/tests/test_mccormick.py index e040ed3fa9b..efb520e5891 100644 --- a/pyomo/contrib/coramin/relaxations/tests/test_mccormick.py +++ b/pyomo/contrib/coramin/relaxations/tests/test_mccormick.py @@ -1,6 +1,6 @@ import pyomo.environ as pyo from pyomo.common import unittest -import coramin +from pyomo.contrib import coramin class TestMcCormick(unittest.TestCase): diff --git a/pyomo/contrib/coramin/relaxations/tests/test_relaxations.py b/pyomo/contrib/coramin/relaxations/tests/test_relaxations.py index 1091902893d..a815e8981cc 100644 --- a/pyomo/contrib/coramin/relaxations/tests/test_relaxations.py +++ b/pyomo/contrib/coramin/relaxations/tests/test_relaxations.py @@ -1,6 +1,6 @@ from pyomo.common import unittest import pyomo.environ as pe -import coramin +from pyomo.contrib import coramin from pyomo.core.base.block import _BlockData from pyomo.core.base.var import _GeneralVarData from pyomo.core.expr.numeric_expr import ExpressionBase diff --git a/pyomo/contrib/coramin/relaxations/tests/test_relaxations_base.py b/pyomo/contrib/coramin/relaxations/tests/test_relaxations_base.py index 69f0bff0b94..c39b5fb9650 100644 --- a/pyomo/contrib/coramin/relaxations/tests/test_relaxations_base.py +++ b/pyomo/contrib/coramin/relaxations/tests/test_relaxations_base.py @@ -1,7 +1,7 @@ from pyomo.common import unittest import pyomo.environ as pe from pyomo.opt import assert_optimal_termination -import coramin +from pyomo.contrib import coramin from pyomo.core.base.var import SimpleVar diff --git a/pyomo/contrib/coramin/relaxations/tests/test_univariate_relaxations.py b/pyomo/contrib/coramin/relaxations/tests/test_univariate_relaxations.py index 5f9a532c936..7a69817a278 100644 --- a/pyomo/contrib/coramin/relaxations/tests/test_univariate_relaxations.py +++ b/pyomo/contrib/coramin/relaxations/tests/test_univariate_relaxations.py @@ -1,7 +1,7 @@ from pyomo.common import unittest import math import pyomo.environ as pe -import coramin +from pyomo.contrib import coramin import numpy as np from pyomo.contrib.coramin.relaxations.segments import compute_k_segment_points diff --git a/pyomo/contrib/coramin/third_party/tests/test_minlplib_tools.py b/pyomo/contrib/coramin/third_party/tests/test_minlplib_tools.py index 15b58a33e38..93d0c928776 100644 --- a/pyomo/contrib/coramin/third_party/tests/test_minlplib_tools.py +++ b/pyomo/contrib/coramin/third_party/tests/test_minlplib_tools.py @@ -1,4 +1,4 @@ -import coramin +from pyomo.contrib import coramin from pyomo.common import unittest import os from pyomo.common.fileutils import this_file_dir From 4f81ae96cdec52e32c86464bba253797468fc835 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Fri, 27 Oct 2023 15:24:42 -0600 Subject: [PATCH 005/128] update tests --- pyomo/contrib/coramin/relaxations/tests/test_relaxations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/coramin/relaxations/tests/test_relaxations.py b/pyomo/contrib/coramin/relaxations/tests/test_relaxations.py index a815e8981cc..21a930e27ed 100644 --- a/pyomo/contrib/coramin/relaxations/tests/test_relaxations.py +++ b/pyomo/contrib/coramin/relaxations/tests/test_relaxations.py @@ -824,7 +824,7 @@ def test_bilinear_relaxation(self): self.large_bounds_helper(m, m.rel, lb=-1e6, ub=1e6) self.small_coef_helper(m, m.rel, e) self.original_constraint_helper(m, m.rel, e) - with self.assertRaisesRegex(ValueError, "Relaxations of type do not support relaxations that are not linear."): + with self.assertRaisesRegex(ValueError, "Relaxations of type do not support relaxations that are not linear."): self.nonlinear_relaxation_helper(m, m.rel, e) self.relaxation_side_helper(m, m.rel, e, check_nonlinear_relaxation=False) self.deviation_helper(m.rel, e) From 0c857d001e35d11ccbba236670db1a6d0c198c67 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Fri, 27 Oct 2023 16:32:27 -0600 Subject: [PATCH 006/128] fix tests --- pyomo/contrib/coramin/domain_reduction/dbt.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/pyomo/contrib/coramin/domain_reduction/dbt.py b/pyomo/contrib/coramin/domain_reduction/dbt.py index 2b659431f1a..92ee35b95eb 100644 --- a/pyomo/contrib/coramin/domain_reduction/dbt.py +++ b/pyomo/contrib/coramin/domain_reduction/dbt.py @@ -436,26 +436,25 @@ def _refine_partition(graph: nx.Graph, model: _BlockData, new_c1 = model.dbt_partition_cons.add(graph_a_var == sum(graph_a_args)) new_c2 = model.dbt_partition_cons.add(graph_b_var == sum(graph_b_args)) if c.equality: - new_c3 = model.dbt_partition_cons.add(graph_a_var + graph_b_var == c.lower) + c.set_value(graph_a_var + graph_b_var == c.lower) else: - new_c3 = model.dbt_partition_cons.add((c.lower, graph_a_var + graph_b_var, c.upper)) + c.set_value((c.lower, graph_a_var + graph_b_var, c.upper)) elif c.lower is None: assert c.upper is not None new_c1 = model.dbt_partition_cons.add(graph_a_var >= sum(graph_a_args)) new_c2 = model.dbt_partition_cons.add(graph_b_var >= sum(graph_b_args)) - new_c3 = model.dbt_partition_cons.add(graph_a_var + graph_b_var <= c.upper) + c.set_value(graph_a_var + graph_b_var <= c.upper) else: assert c.upper is None new_c1 = model.dbt_partition_cons.add(graph_a_var <= sum(graph_a_args)) new_c2 = model.dbt_partition_cons.add(graph_b_var <= sum(graph_b_args)) - new_c3 = model.dbt_partition_cons.add(graph_a_var + graph_b_var >= c.lower) - c.deactivate() + c.set_value(graph_a_var + graph_b_var >= c.lower) # update the graph graph.remove_node(_ConNode(c)) graph.add_node(_VarNode(graph_a_var)) graph.add_node(_VarNode(graph_b_var)) - for new_con in [new_c1, new_c2, new_c3]: + for new_con in [new_c1, new_c2, c]: graph.add_node(_ConNode(new_con)) for v in identify_variables(new_con.body, include_fixed=False): graph.add_edge(_VarNode(v), _ConNode(new_con)) @@ -466,7 +465,7 @@ def _refine_partition(graph: nx.Graph, model: _BlockData, if e.node2.comp is not c: new_removed_edges.append(e) - new_removed_edges.append(_Edge(_VarNode(graph_a_var), _ConNode(new_c3))) + new_removed_edges.append(_Edge(_VarNode(graph_a_var), _ConNode(c))) removed_edges = new_removed_edges # update graph_a_nodes and graph_b_nodes @@ -476,7 +475,7 @@ def _refine_partition(graph: nx.Graph, model: _BlockData, graph_b_nodes.add(_VarNode(graph_b_var)) graph_a_nodes.add(_ConNode(new_c1)) graph_b_nodes.add(_ConNode(new_c2)) - graph_b_nodes.add(_ConNode(new_c3)) + graph_b_nodes.add(_ConNode(c)) return removed_edges From 2755bac7106f249fdd5f69ab00582befb460eebb Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Fri, 27 Oct 2023 16:36:24 -0600 Subject: [PATCH 007/128] update tests --- pyomo/contrib/coramin/domain_reduction/tests/test_dbt.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pyomo/contrib/coramin/domain_reduction/tests/test_dbt.py b/pyomo/contrib/coramin/domain_reduction/tests/test_dbt.py index 6ed3d765484..e71e3ad6ef0 100644 --- a/pyomo/contrib/coramin/domain_reduction/tests/test_dbt.py +++ b/pyomo/contrib/coramin/domain_reduction/tests/test_dbt.py @@ -786,9 +786,7 @@ def create_model(self): return m - @pytest.mark.parallel - @pytest.mark.two_proc - @pytest.mark.three_proc + @pytest.mark.mpi def test_bounds_tightening(self): from mpi4py import MPI From 255bc458dd0cbba0e1385200ba161089dccce620 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Fri, 27 Oct 2023 16:45:34 -0600 Subject: [PATCH 008/128] run black --- pyomo/contrib/coramin/__init__.py | 2 +- .../coramin/algorithms/multitree/multitree.py | 163 +++- .../multitree/tests/test_multitree.py | 133 ++-- .../algorithms/tests/test_ecp_bounder.py | 8 +- pyomo/contrib/coramin/clone.py | 11 +- .../coramin/domain_reduction/__init__.py | 20 +- pyomo/contrib/coramin/domain_reduction/dbt.py | 624 ++++++++++----- .../coramin/domain_reduction/filters.py | 38 +- .../contrib/coramin/domain_reduction/obbt.py | 218 ++++-- .../domain_reduction/tests/test_dbt.py | 440 +++++++---- .../domain_reduction/tests/test_filters.py | 10 +- .../domain_reduction/tests/test_obbt.py | 32 +- pyomo/contrib/coramin/examples/alpha_bb.py | 5 +- pyomo/contrib/coramin/examples/dbt.py | 54 +- pyomo/contrib/coramin/examples/dbt2.py | 66 +- pyomo/contrib/coramin/examples/ex.py | 18 +- pyomo/contrib/coramin/examples/rosenbrock.py | 4 +- pyomo/contrib/coramin/relaxations/__init__.py | 7 +- pyomo/contrib/coramin/relaxations/_utils.py | 23 +- pyomo/contrib/coramin/relaxations/alphabb.py | 23 +- .../contrib/coramin/relaxations/auto_relax.py | 728 +++++++++++++----- .../coramin/relaxations/copy_relaxation.py | 105 ++- .../coramin/relaxations/custom_block.py | 28 +- pyomo/contrib/coramin/relaxations/hessian.py | 16 +- .../contrib/coramin/relaxations/mccormick.py | 187 +++-- .../coramin/relaxations/multivariate.py | 63 +- .../coramin/relaxations/relaxations_base.py | 227 ++++-- .../coramin/relaxations/tests/test_alphabb.py | 7 +- .../relaxations/tests/test_auto_relax.py | 307 +++++--- .../coramin/relaxations/tests/test_copy.py | 148 ++-- .../relaxations/tests/test_iterators.py | 70 +- .../relaxations/tests/test_mccormick.py | 21 +- .../relaxations/tests/test_relaxations.py | 562 ++++++++++---- .../tests/test_relaxations_base.py | 8 +- .../tests/test_univariate_relaxations.py | 205 +++-- .../contrib/coramin/relaxations/univariate.py | 582 ++++++++++---- pyomo/contrib/coramin/third_party/__init__.py | 6 +- .../coramin/third_party/minlplib_tools.py | 239 ++++-- .../third_party/tests/test_minlplib_tools.py | 328 +++++--- pyomo/contrib/coramin/utils/mpi_utils.py | 27 +- .../contrib/coramin/utils/plot_relaxation.py | 47 +- pyomo/contrib/coramin/utils/pyomo_utils.py | 4 +- 42 files changed, 4141 insertions(+), 1673 deletions(-) diff --git a/pyomo/contrib/coramin/__init__.py b/pyomo/contrib/coramin/__init__.py index 6e08922d11f..b87e7910841 100644 --- a/pyomo/contrib/coramin/__init__.py +++ b/pyomo/contrib/coramin/__init__.py @@ -9,5 +9,5 @@ Effort, EigenValueBounder, simplify_expr, - get_objective + get_objective, ) diff --git a/pyomo/contrib/coramin/algorithms/multitree/multitree.py b/pyomo/contrib/coramin/algorithms/multitree/multitree.py index a74343c2ae0..0533c8aaac0 100644 --- a/pyomo/contrib/coramin/algorithms/multitree/multitree.py +++ b/pyomo/contrib/coramin/algorithms/multitree/multitree.py @@ -18,13 +18,26 @@ from pyomo.contrib import appsi from typing import Tuple, Optional, MutableMapping, Sequence from pyomo.common.config import ( - ConfigValue, NonNegativeInt, PositiveFloat, PositiveInt, NonNegativeFloat, InEnum + ConfigValue, + NonNegativeInt, + PositiveFloat, + PositiveInt, + NonNegativeFloat, + InEnum, ) import logging from pyomo.contrib.coramin.relaxations.auto_relax import relax from pyomo.contrib.coramin.relaxations.iterators import relaxation_data_objects -from pyomo.contrib.coramin.utils.coramin_enums import RelaxationSide, Effort, EigenValueBounder -from pyomo.contrib.coramin.domain_reduction.dbt import push_integers, pop_integers, collect_vars_to_tighten +from pyomo.contrib.coramin.utils.coramin_enums import ( + RelaxationSide, + Effort, + EigenValueBounder, +) +from pyomo.contrib.coramin.domain_reduction.dbt import ( + push_integers, + pop_integers, + collect_vars_to_tighten, +) from pyomo.contrib.coramin.domain_reduction.obbt import perform_obbt import time from pyomo.core.base.var import _GeneralVarData @@ -206,9 +219,15 @@ def _should_terminate(self) -> Tuple[bool, Optional[TerminationCondition]]: primal_bound = self._get_primal_bound() dual_bound = self._get_dual_bound() if self._objective.sense == pe.minimize: - assert primal_bound >= dual_bound - 1e-6*max(abs(primal_bound), abs(dual_bound)) - 1e-6 + assert ( + primal_bound + >= dual_bound - 1e-6 * max(abs(primal_bound), abs(dual_bound)) - 1e-6 + ) else: - assert primal_bound <= dual_bound + 1e-6*max(abs(primal_bound), abs(dual_bound)) + 1e-6 + assert ( + primal_bound + <= dual_bound + 1e-6 * max(abs(primal_bound), abs(dual_bound)) + 1e-6 + ) abs_gap, rel_gap = self._get_abs_and_rel_gap() if abs_gap <= self.config.abs_gap: return True, TerminationCondition.optimal @@ -216,7 +235,9 @@ def _should_terminate(self) -> Tuple[bool, Optional[TerminationCondition]]: return True, TerminationCondition.optimal return False, TerminationCondition.unknown - def _get_results(self, termination_condition: TerminationCondition) -> MultiTreeResults: + def _get_results( + self, termination_condition: TerminationCondition + ) -> MultiTreeResults: res = MultiTreeResults() res.termination_condition = termination_condition res.best_feasible_objective = self._best_feasible_objective @@ -228,13 +249,17 @@ def _get_results(self, termination_condition: TerminationCondition) -> MultiTree if self.config.load_solution: if res.best_feasible_objective is not None: if res.termination_condition != TerminationCondition.optimal: - logger.warning('Loading a feasible but potentially sub-optimal ' - 'solution. Please check the termination condition.') + logger.warning( + 'Loading a feasible but potentially sub-optimal ' + 'solution. Please check the termination condition.' + ) res.solution_loader.load_vars() else: - raise RuntimeError('No feasible solution was found. Please ' - 'set opt.config.load_solution=False and check the ' - 'termination condition before loading a solution.') + raise RuntimeError( + 'No feasible solution was found. Please ' + 'set opt.config.load_solution=False and check the ' + 'termination condition before loading a solution.' + ) return res @@ -289,9 +314,17 @@ def _get_constr_violation(self): viol_list.append(b.get_deviation()) return max(viol_list) - def _log(self, header=False, num_lb_improved=0, num_ub_improved=0, - avg_lb_improvement=0, avg_ub_improvement=0, rel_termination=None, - nlp_termination=None, constr_viol=None): + def _log( + self, + header=False, + num_lb_improved=0, + num_ub_improved=0, + avg_lb_improvement=0, + avg_ub_improvement=0, + rel_termination=None, + nlp_termination=None, + constr_viol=None, + ): logger = self.config.solver_output_logger log_level = self.config.log_level if header: @@ -327,7 +360,7 @@ def _log(self, header=False, num_lb_improved=0, num_ub_improved=0, elapsed_time_str = f'{elapsed_time:<6.2f}' else: elapsed_time_str = f'{round(elapsed_time):<6d}' - percent_gap = rel_gap*100 + percent_gap = rel_gap * 100 if math.isinf(percent_gap): percent_gap_str = f'{percent_gap:<7.2f}' elif percent_gap >= 100: @@ -372,15 +405,29 @@ def _update_dual_bound(self, res: Results): if v.value is None: assert v.stale continue - if not math.isclose(v.value, round(v.value), rel_tol=self.config.integer_tolerance, abs_tol=self.config.integer_tolerance): + if not math.isclose( + v.value, + round(v.value), + rel_tol=self.config.integer_tolerance, + abs_tol=self.config.integer_tolerance, + ): all_cons_satisfied = False break if all_cons_satisfied: for rel_v, nlp_v in self._rel_to_nlp_map.items(): if rel_v.value is None: assert rel_v.stale - if rel_v.has_lb() and rel_v.has_ub() and math.isclose(rel_v.lb, rel_v.ub, rel_tol=self.config.feasibility_tolerance, abs_tol=self.config.feasibility_tolerance): - nlp_v.value = 0.5*(rel_v.lb + rel_v.ub) + if ( + rel_v.has_lb() + and rel_v.has_ub() + and math.isclose( + rel_v.lb, + rel_v.ub, + rel_tol=self.config.feasibility_tolerance, + abs_tol=self.config.feasibility_tolerance, + ) + ): + nlp_v.value = 0.5 * (rel_v.lb + rel_v.ub) else: nlp_v.value = None else: @@ -424,7 +471,12 @@ def _solve_nlp_with_fixed_vars( if v.fixed: continue val = integer_var_values[v] - assert math.isclose(val, round(val), rel_tol=self.config.integer_tolerance, abs_tol=self.config.integer_tolerance) + assert math.isclose( + val, + round(val), + rel_tol=self.config.integer_tolerance, + abs_tol=self.config.integer_tolerance, + ) val = round(val) nlp_v = self._rel_to_nlp_map[v] orig_v = self._nlp_to_orig_map[nlp_v] @@ -482,7 +534,12 @@ def _solve_nlp_with_fixed_vars( if v.fixed: continue if v.has_lb() and v.has_ub(): - if math.isclose(v.lb, v.ub, rel_tol=self.config.feasibility_tolerance, abs_tol=self.config.feasibility_tolerance): + if math.isclose( + v.lb, + v.ub, + rel_tol=self.config.feasibility_tolerance, + abs_tol=self.config.feasibility_tolerance, + ): v.fix(0.5 * (v.lb + v.ub)) fixed_vars.append(v) else: @@ -533,7 +590,14 @@ def _solve_nlp_with_fixed_vars( nlp_res.termination_condition = TerminationCondition.optimal nlp_res.best_feasible_objective = pe.value(nlp_obj) nlp_res.best_objective_bound = nlp_res.best_feasible_objective - nlp_res.solution_loader = MultiTreeSolutionLoader(pe.ComponentMap((v, v.value) for v in self._nlp.component_data_objects(pe.Var, descend_into=True))) + nlp_res.solution_loader = MultiTreeSolutionLoader( + pe.ComponentMap( + (v, v.value) + for v in self._nlp.component_data_objects( + pe.Var, descend_into=True + ) + ) + ) self._update_primal_bound(nlp_res) self._log(header=False, nlp_termination=nlp_res.termination_condition) @@ -558,7 +622,11 @@ def _solve_relaxation(self) -> Results: rel_res.solution_loader.load_vars() self._update_dual_bound(rel_res) - self._log(header=False, rel_termination=rel_res.termination_condition, constr_viol=self._get_constr_violation()) + self._log( + header=False, + rel_termination=rel_res.termination_condition, + constr_viol=self._get_constr_violation(), + ) if rel_res.termination_condition not in { TerminationCondition.optimal, TerminationCondition.maxTimeLimit, @@ -580,7 +648,8 @@ def _partition_helper(self): logger.error( 'The multitree algorithm is not guaranteed to converge ' 'for problems with unbounded variables. Please bound all ' - 'variables.') + 'variables.' + ) self._stop = TerminationCondition.error err = True break @@ -740,7 +809,9 @@ def _construct_nlp(self): nlp_solver.config = self.nlp_solver.config() eigenvalue_opt = MultiTree(mip_solver=mip_solver, nlp_solver=nlp_solver) eigenvalue_opt.config = self.config() - eigenvalue_opt.config.convexity_effort = min(self.config.convexity_effort, Effort.medium) + eigenvalue_opt.config.convexity_effort = min( + self.config.convexity_effort, Effort.medium + ) self._nlp = relax( model=self._original_model, @@ -764,9 +835,7 @@ def _construct_nlp(self): def _construct_relaxation(self): all_vars = list( - ComponentSet( - self._nlp.component_data_objects(pe.Var, descend_into=True) - ) + ComponentSet(self._nlp.component_data_objects(pe.Var, descend_into=True)) ) tmp_name = unique_component_name(self._nlp, "all_vars") setattr(self._nlp, tmp_name, all_vars) @@ -776,7 +845,9 @@ def _construct_relaxation(self): delattr(self._nlp, tmp_name) delattr(self._relaxation, tmp_name) - for b in relaxation_data_objects(self._relaxation, descend_into=True, active=True): + for b in relaxation_data_objects( + self._relaxation, descend_into=True, active=True + ): b.small_coef = self.config.small_coef b.large_coef = self.config.large_coef b.safety_tol = self.config.safety_tol @@ -830,11 +901,14 @@ def _perform_obbt(self, vars_to_tighten): orig_ubs.append(v_ub) orig_lbs = np.array(orig_lbs) orig_ubs = np.array(orig_ubs) - perform_obbt(self._relaxation, solver=self.mip_solver, - varlist=list(vars_to_tighten), - objective_bound=self._best_feasible_objective, - with_progress_bar=self.config.show_obbt_progress_bar, - time_limit=self._remaining_time) + perform_obbt( + self._relaxation, + solver=self.mip_solver, + varlist=list(vars_to_tighten), + objective_bound=self._best_feasible_objective, + with_progress_bar=self.config.show_obbt_progress_bar, + time_limit=self._remaining_time, + ) new_lbs = list() new_ubs = list() for ndx, v in enumerate(vars_to_tighten): @@ -873,14 +947,19 @@ def _perform_obbt(self, vars_to_tighten): avg_ub_improvement = np.mean(ub_diff[ub_improved_indices]) else: avg_ub_improvement = 0 - self._log(header=False, num_lb_improved=num_lb_improved, - num_ub_improved=num_ub_improved, - avg_lb_improvement=avg_lb_improvement, - avg_ub_improvement=avg_ub_improvement) + self._log( + header=False, + num_lb_improved=num_lb_improved, + num_ub_improved=num_ub_improved, + avg_lb_improvement=avg_lb_improvement, + avg_ub_improvement=avg_ub_improvement, + ) return num_lb_improved, num_ub_improved, avg_lb_improvement, avg_ub_improvement - def solve(self, model: _BlockData, timer: HierarchicalTimer = None) -> MultiTreeResults: + def solve( + self, model: _BlockData, timer: HierarchicalTimer = None + ) -> MultiTreeResults: model = clone_active_flat(model) self._re_init() @@ -982,9 +1061,13 @@ def solve(self, model: _BlockData, timer: HierarchicalTimer = None) -> MultiTree if should_terminate: break - if self.config.obbt_at_new_incumbents and not math.isclose(start_primal_bound, end_primal_bound, rel_tol=1e-4, abs_tol=1e-4): + if self.config.obbt_at_new_incumbents and not math.isclose( + start_primal_bound, end_primal_bound, rel_tol=1e-4, abs_tol=1e-4 + ): if self.config.relax_integers_for_obbt: - relaxed_binaries, relaxed_integers = push_integers(self._relaxation) + relaxed_binaries, relaxed_integers = push_integers( + self._relaxation + ) num_lb, num_ub, avg_lb, avg_ub = self._perform_obbt(vars_to_tighten) if self.config.relax_integers_for_obbt: pop_integers(relaxed_binaries, relaxed_integers) diff --git a/pyomo/contrib/coramin/algorithms/multitree/tests/test_multitree.py b/pyomo/contrib/coramin/algorithms/multitree/tests/test_multitree.py index 972a1cf4b49..97954565f95 100644 --- a/pyomo/contrib/coramin/algorithms/multitree/tests/test_multitree.py +++ b/pyomo/contrib/coramin/algorithms/multitree/tests/test_multitree.py @@ -1,7 +1,10 @@ import math from pyomo.contrib import coramin -from pyomo.contrib.coramin.third_party.minlplib_tools import get_minlplib, get_minlplib_instancedata +from pyomo.contrib.coramin.third_party.minlplib_tools import ( + get_minlplib, + get_minlplib_instancedata, +) from pyomo.common import unittest from pyomo.contrib import appsi import os @@ -39,19 +42,24 @@ def _check_relative_diff(self, expected, got, abs_tol=1e-3, rel_tol=1e-3): else: rel_diff = abs_diff / abs(expected) success = abs_diff <= abs_tol or rel_diff <= rel_tol - self.assertTrue(success, msg=f'\n expected: {expected}\n got: {got}\n abs diff: {abs_diff}\n rel diff: {rel_diff}') + self.assertTrue( + success, + msg=f'\n expected: {expected}\n got: {got}\n abs diff: {abs_diff}\n rel diff: {rel_diff}', + ) class TestMultiTreeWithMINLPLib(Helper): @classmethod def setUpClass(self) -> None: - self.test_problems = {'batch': 285506.5082, - 'ball_mk3_10': None, - 'ball_mk2_10': 0, - 'syn05m': 837.73240090, - 'autocorr_bern20-03': -72, - 'chem': -47.70651483, - 'alkyl': -1.76499965} + self.test_problems = { + 'batch': 285506.5082, + 'ball_mk3_10': None, + 'ball_mk2_10': 0, + 'syn05m': 837.73240090, + 'autocorr_bern20-03': -72, + 'chem': -47.70651483, + 'alkyl': -1.76499965, + } self.primal_sol = dict() self.primal_sol['batch'] = _get_sol('batch') self.primal_sol['alkyl'] = _get_sol('alkyl') @@ -64,8 +72,9 @@ def setUpClass(self) -> None: mip_solver = appsi.solvers.Gurobi() nlp_solver = appsi.solvers.Ipopt() nlp_solver.config.log_level = logging.DEBUG - self.opt = coramin.algorithms.MultiTree(mip_solver=mip_solver, - nlp_solver=nlp_solver) + self.opt = coramin.algorithms.MultiTree( + mip_solver=mip_solver, nlp_solver=nlp_solver + ) @classmethod def tearDownClass(self) -> None: @@ -99,11 +108,21 @@ def _check_primal_sol(self, pname, m: _BlockData, res: appsi.base.Results): def optimal_helper(self, pname, check_primal_sol=True): m = self.get_model(pname) res = self.opt.solve(m) - self.assertEqual(res.termination_condition, appsi.base.TerminationCondition.optimal) - self._check_relative_diff(self.test_problems[pname], res.best_feasible_objective, - abs_tol=self.opt.config.abs_gap, rel_tol=self.opt.config.mip_gap) - self._check_relative_diff(self.test_problems[pname], res.best_objective_bound, - abs_tol=self.opt.config.abs_gap, rel_tol=self.opt.config.mip_gap) + self.assertEqual( + res.termination_condition, appsi.base.TerminationCondition.optimal + ) + self._check_relative_diff( + self.test_problems[pname], + res.best_feasible_objective, + abs_tol=self.opt.config.abs_gap, + rel_tol=self.opt.config.mip_gap, + ) + self._check_relative_diff( + self.test_problems[pname], + res.best_objective_bound, + abs_tol=self.opt.config.abs_gap, + rel_tol=self.opt.config.mip_gap, + ) if check_primal_sol: self._check_primal_sol(pname, m, res) @@ -111,7 +130,9 @@ def infeasible_helper(self, pname): m = self.get_model(pname) self.opt.config.load_solution = False res = self.opt.solve(m) - self.assertEqual(res.termination_condition, appsi.base.TerminationCondition.infeasible) + self.assertEqual( + res.termination_condition, appsi.base.TerminationCondition.infeasible + ) self.opt.config.load_solution = True def time_limit_helper(self, pname): @@ -121,7 +142,9 @@ def time_limit_helper(self, pname): self.opt.config.time_limit = new_limit m = self.get_model(pname) res = self.opt.solve(m) - self.assertEqual(res.termination_condition, appsi.base.TerminationCondition.maxTimeLimit) + self.assertEqual( + res.termination_condition, appsi.base.TerminationCondition.maxTimeLimit + ) self.opt.config.load_solution = True def test_batch(self): @@ -162,19 +185,28 @@ def test_convex_overestimator(self): m = pe.ConcreteModel() m.x = pe.Var(bounds=(-2, 1)) m.y = pe.Var() - m.obj = pe.Objective(expr=(m.x + 1)**2 - 0.2*m.y) + m.obj = pe.Objective(expr=(m.x + 1) ** 2 - 0.2 * m.y) m.c = pe.Constraint(expr=m.y <= m.x**2) mip_solver = appsi.solvers.Gurobi() nlp_solver = appsi.solvers.Ipopt() nlp_solver.config.log_level = logging.DEBUG - opt = coramin.algorithms.MultiTree(mip_solver=mip_solver, - nlp_solver=nlp_solver) + opt = coramin.algorithms.MultiTree(mip_solver=mip_solver, nlp_solver=nlp_solver) res = opt.solve(m) - self.assertEqual(res.termination_condition, appsi.base.TerminationCondition.optimal) - self._check_relative_diff(-0.25, res.best_feasible_objective, - abs_tol=opt.config.abs_gap, rel_tol=opt.config.mip_gap) - self._check_relative_diff(-0.25, res.best_objective_bound, - abs_tol=opt.config.abs_gap, rel_tol=opt.config.mip_gap) + self.assertEqual( + res.termination_condition, appsi.base.TerminationCondition.optimal + ) + self._check_relative_diff( + -0.25, + res.best_feasible_objective, + abs_tol=opt.config.abs_gap, + rel_tol=opt.config.mip_gap, + ) + self._check_relative_diff( + -0.25, + res.best_objective_bound, + abs_tol=opt.config.abs_gap, + rel_tol=opt.config.mip_gap, + ) self._check_relative_diff(-1.250953, m.x.value, 1e-2, 1e-2) self._check_relative_diff(1.5648825, m.y.value, 1e-2, 1e-2) @@ -182,56 +214,61 @@ def test_max_iter(self): m = pe.ConcreteModel() m.x = pe.Var(bounds=(-2, 1)) m.y = pe.Var() - m.obj = pe.Objective(expr=(m.x + 1)**2 - 0.2*m.y) + m.obj = pe.Objective(expr=(m.x + 1) ** 2 - 0.2 * m.y) m.c = pe.Constraint(expr=m.y <= m.x**2) mip_solver = appsi.solvers.Gurobi() nlp_solver = appsi.solvers.Ipopt() nlp_solver.config.log_level = logging.DEBUG - opt = coramin.algorithms.MultiTree(mip_solver=mip_solver, - nlp_solver=nlp_solver) + opt = coramin.algorithms.MultiTree(mip_solver=mip_solver, nlp_solver=nlp_solver) opt.config.max_iter = 3 opt.config.load_solution = False res = opt.solve(m) - self.assertEqual(res.termination_condition, appsi.base.TerminationCondition.maxIterations) + self.assertEqual( + res.termination_condition, appsi.base.TerminationCondition.maxIterations + ) self.assertIsNone(res.best_feasible_objective) opt.config.max_iter = 10 res = opt.solve(m) - self.assertEqual(res.termination_condition, appsi.base.TerminationCondition.maxIterations) + self.assertEqual( + res.termination_condition, appsi.base.TerminationCondition.maxIterations + ) self.assertIsNotNone(res.best_feasible_objective) def test_nlp_infeas_fbbt(self): m = pe.ConcreteModel() m.x = pe.Var(bounds=(-2, 1), domain=pe.Integers) m.y = pe.Var(domain=pe.Integers) - m.obj = pe.Objective(expr=(m.x + 1)**2 - 0.2*m.y) - m.c1 = pe.Constraint(expr=m.y <= (m.x - 0.5)**2 - 0.5) - m.c2 = pe.Constraint(expr=m.y >= -(m.x + 2)**2 + 4) - m.c3 = pe.Constraint(expr=m.y <= 2*m.x + 7) + m.obj = pe.Objective(expr=(m.x + 1) ** 2 - 0.2 * m.y) + m.c1 = pe.Constraint(expr=m.y <= (m.x - 0.5) ** 2 - 0.5) + m.c2 = pe.Constraint(expr=m.y >= -((m.x + 2) ** 2) + 4) + m.c3 = pe.Constraint(expr=m.y <= 2 * m.x + 7) m.c4 = pe.Constraint(expr=m.y >= m.x) mip_solver = appsi.solvers.Gurobi() nlp_solver = appsi.solvers.Ipopt() nlp_solver.config.log_level = logging.DEBUG - opt = coramin.algorithms.MultiTree(mip_solver=mip_solver, - nlp_solver=nlp_solver) + opt = coramin.algorithms.MultiTree(mip_solver=mip_solver, nlp_solver=nlp_solver) opt.config.load_solution = False res = opt.solve(m) - self.assertEqual(res.termination_condition, appsi.base.TerminationCondition.infeasible) + self.assertEqual( + res.termination_condition, appsi.base.TerminationCondition.infeasible + ) def test_all_vars_fixed_in_nlp(self): m = pe.ConcreteModel() m.x = pe.Var(bounds=(-2, 1)) m.y = pe.Var(domain=pe.Integers) m.z = pe.Var() - m.obj = pe.Objective(expr=m.z - 0.2*m.y) - m.c1 = pe.Constraint(expr=m.y == (m.x - 0.5)**2 - 0.5) - m.c2 = pe.Constraint(expr=m.z == (m.x + 1)**2) + m.obj = pe.Objective(expr=m.z - 0.2 * m.y) + m.c1 = pe.Constraint(expr=m.y == (m.x - 0.5) ** 2 - 0.5) + m.c2 = pe.Constraint(expr=m.z == (m.x + 1) ** 2) mip_solver = appsi.solvers.Gurobi() nlp_solver = appsi.solvers.Ipopt() nlp_solver.config.log_level = logging.DEBUG - opt = coramin.algorithms.MultiTree(mip_solver=mip_solver, - nlp_solver=nlp_solver) + opt = coramin.algorithms.MultiTree(mip_solver=mip_solver, nlp_solver=nlp_solver) res = opt.solve(m) - self.assertEqual(res.termination_condition, appsi.base.TerminationCondition.optimal) + self.assertEqual( + res.termination_condition, appsi.base.TerminationCondition.optimal + ) self._check_relative_diff(-0.462486082, res.best_feasible_objective) self._check_relative_diff(-0.462486082, res.best_objective_bound) self._check_relative_diff(-1.37082869, m.x.value) @@ -249,7 +286,9 @@ def test_linear_problem(self): nlp_solver = appsi.solvers.Ipopt() opt = coramin.algorithms.MultiTree(mip_solver=mip_solver, nlp_solver=nlp_solver) res = opt.solve(m) - self.assertEqual(res.termination_condition, appsi.base.TerminationCondition.optimal) + self.assertEqual( + res.termination_condition, appsi.base.TerminationCondition.optimal + ) self.assertAlmostEqual(res.best_feasible_objective, 1) self.assertAlmostEqual(res.best_objective_bound, 1) self.assertAlmostEqual(m.x.value, 0) @@ -269,7 +308,9 @@ def test_stale_fixed_vars(self): nlp_solver = appsi.solvers.Ipopt() opt = coramin.algorithms.MultiTree(mip_solver=mip_solver, nlp_solver=nlp_solver) res = opt.solve(m) - self.assertEqual(res.termination_condition, appsi.base.TerminationCondition.optimal) + self.assertEqual( + res.termination_condition, appsi.base.TerminationCondition.optimal + ) self.assertAlmostEqual(res.best_feasible_objective, 1) self.assertAlmostEqual(res.best_objective_bound, 1) self.assertAlmostEqual(m.x.value, 0) diff --git a/pyomo/contrib/coramin/algorithms/tests/test_ecp_bounder.py b/pyomo/contrib/coramin/algorithms/tests/test_ecp_bounder.py index 8b841999549..538c58c7101 100644 --- a/pyomo/contrib/coramin/algorithms/tests/test_ecp_bounder.py +++ b/pyomo/contrib/coramin/algorithms/tests/test_ecp_bounder.py @@ -14,12 +14,14 @@ def test_ecp_bounder(self): m = pe.ConcreteModel() m.x = pe.Var() m.y = pe.Var() - m.obj = pe.Objective(expr=0.5*(m.x**2 + m.y**2)) - m.c1 = pe.Constraint(expr=m.y >= (m.x - 1)**2) + m.obj = pe.Objective(expr=0.5 * (m.x**2 + m.y**2)) + m.c1 = pe.Constraint(expr=m.y >= (m.x - 1) ** 2) m.c2 = pe.Constraint(expr=m.y >= pe.exp(m.x)) coramin.relaxations.relax(m, in_place=True) opt = ECPBounder(subproblem_solver=appsi.solvers.Gurobi()) res = opt.solve(m) - self.assertEqual(res.termination_condition, appsi.base.TerminationCondition.optimal) + self.assertEqual( + res.termination_condition, appsi.base.TerminationCondition.optimal + ) self.assertAlmostEqual(m.x.value, 0) self.assertAlmostEqual(m.y.value, 1) diff --git a/pyomo/contrib/coramin/clone.py b/pyomo/contrib/coramin/clone.py index df15b5123d6..b624abd7643 100644 --- a/pyomo/contrib/coramin/clone.py +++ b/pyomo/contrib/coramin/clone.py @@ -13,10 +13,7 @@ def clone_active_flat(m1): # constraints for c in iterators.nonrelaxation_component_data_objects( - m1, - pe.Constraint, - active=True, - descend_into=True, + m1, pe.Constraint, active=True, descend_into=True ): lb = pe.value(c.lower) ub = pe.value(c.upper) @@ -35,11 +32,7 @@ def clone_active_flat(m1): m2.obj = pe.Objective(expr=obj_expr, sense=obj.sense) rel_list = list() - for r in iterators.relaxation_data_objects( - m1, - descend_into=True, - active=True, - ): + for r in iterators.relaxation_data_objects(m1, descend_into=True, active=True): rel_list.append(r) for ndx, r in enumerate(rel_list): diff --git a/pyomo/contrib/coramin/domain_reduction/__init__.py b/pyomo/contrib/coramin/domain_reduction/__init__.py index 0668d94bd8e..02d751b7292 100644 --- a/pyomo/contrib/coramin/domain_reduction/__init__.py +++ b/pyomo/contrib/coramin/domain_reduction/__init__.py @@ -1,8 +1,22 @@ from .obbt import perform_obbt from .filters import filter_variables_from_solution, aggressive_filter + try: - from .dbt import decompose_model, perform_dbt, perform_dbt_with_integers_relaxed, TreeBlockData, TreeBlock, \ - DecompositionError, TreeBlockError, collect_vars_to_tighten, collect_vars_to_tighten_by_block, DBTInfo, \ - push_integers, pop_integers, OBBTMethod, FilterMethod + from .dbt import ( + decompose_model, + perform_dbt, + perform_dbt_with_integers_relaxed, + TreeBlockData, + TreeBlock, + DecompositionError, + TreeBlockError, + collect_vars_to_tighten, + collect_vars_to_tighten_by_block, + DBTInfo, + push_integers, + pop_integers, + OBBTMethod, + FilterMethod, + ) except: pass diff --git a/pyomo/contrib/coramin/domain_reduction/dbt.py b/pyomo/contrib/coramin/domain_reduction/dbt.py index 92ee35b95eb..80596eb4994 100644 --- a/pyomo/contrib/coramin/domain_reduction/dbt.py +++ b/pyomo/contrib/coramin/domain_reduction/dbt.py @@ -11,21 +11,28 @@ from pyomo.core.expr.visitor import identify_variables from pyomo.common.collections import ComponentSet from pyomo.common.collections.orderedset import OrderedSet -from pyomo.contrib.coramin.relaxations.iterators import relaxation_data_objects, nonrelaxation_component_data_objects +from pyomo.contrib.coramin.relaxations.iterators import ( + relaxation_data_objects, + nonrelaxation_component_data_objects, +) from pyomo.core.expr.visitor import replace_expressions import logging import networkx + try: import metis + metis_available = True except ImportError: - metis_available = False + metis_available = False import numpy as np import math from pyomo.core.base.block import declare_custom_block, _BlockData from pyomo.contrib.coramin.utils.pyomo_utils import get_objective from pyomo.core.base.var import _GeneralVarData -from pyomo.contrib.coramin.relaxations.copy_relaxation import copy_relaxation_with_local_data +from pyomo.contrib.coramin.relaxations.copy_relaxation import ( + copy_relaxation_with_local_data, +) from pyomo.contrib.coramin.relaxations.relaxations_base import BaseRelaxationData from pyomo.contrib.coramin.utils import RelaxationSide from collections import defaultdict @@ -79,7 +86,9 @@ def setup(self, children_keys): def _assert_setup(self): if not self._already_setup: - raise TreeBlockError('The TreeBlock has not been setup yet. Please call the setup method.') + raise TreeBlockError( + 'The TreeBlock has not been setup yet. Please call the setup method.' + ) def is_leaf(self): self._assert_setup() @@ -87,23 +96,33 @@ def is_leaf(self): def add_component(self, name, val): self._assert_setup() - if self.is_leaf() or self._allow_changes or not isinstance(val, _GeneralVarData): + if ( + self.is_leaf() + or self._allow_changes + or not isinstance(val, _GeneralVarData) + ): _BlockData.add_component(self, name, val) else: - raise TreeBlockError('Pyomo variables cannot be added to a TreeBlock unless it is a leaf.') + raise TreeBlockError( + 'Pyomo variables cannot be added to a TreeBlock unless it is a leaf.' + ) @property def children(self): self._assert_setup() if self.is_leaf(): - raise TreeBlockError('Leaf TreeBlocks do not have children. Please check the is_leaf method') + raise TreeBlockError( + 'Leaf TreeBlocks do not have children. Please check the is_leaf method' + ) return self._children @property def linking_constraints(self): self._assert_setup() if self.is_leaf(): - raise TreeBlockError('leaf TreeBlocks do not have linking_constraints. Please check the is_leaf method.') + raise TreeBlockError( + 'leaf TreeBlocks do not have linking_constraints. Please check the is_leaf method.' + ) return self._linking_constraints def _num_stages(self): @@ -115,7 +134,9 @@ def _num_stages(self): def num_stages(self): if not self._is_root: - raise TreeBlockError('The num_stages method can only be called from the root TreeBlock') + raise TreeBlockError( + 'The num_stages method can only be called from the root TreeBlock' + ) return self._num_stages() @staticmethod @@ -126,13 +147,17 @@ def _stage_blocks(children, count, stage): else: for child in children.values(): if not child.is_leaf(): - for b in TreeBlockData._stage_blocks(child.children, count+1, stage): + for b in TreeBlockData._stage_blocks( + child.children, count + 1, stage + ): yield b def stage_blocks(self, stage, active=None): self._assert_setup() if not self._is_root: - raise TreeBlockError('The num_stages method can only be called from the root TreeBlock') + raise TreeBlockError( + 'The num_stages method can only be called from the root TreeBlock' + ) if stage == 0: if (active and self.active) or (not active): yield self @@ -144,7 +169,9 @@ def stage_blocks(self, stage, active=None): def get_block_stage(self, block): self._assert_setup() if not self._is_root: - raise TreeBlockError('The get_block_stage method can only be called from the root TreeBlock.') + raise TreeBlockError( + 'The get_block_stage method can only be called from the root TreeBlock.' + ) for stage_ndx in range(self.num_stages()): stage_blocks = OrderedSet(self.stage_blocks(stage_ndx)) if block in stage_blocks: @@ -241,19 +268,29 @@ def build_pyomo_model(self, block): tmp_component_map = child.build_pyomo_model(block=block.children[i]) elif isinstance(child, networkx.Graph): block.children[i].setup(children_keys=list()) - tmp_component_map = build_pyomo_model_from_graph(graph=child, block=block.children[i]) + tmp_component_map = build_pyomo_model_from_graph( + graph=child, block=block.children[i] + ) else: raise ValueError('Unexpected child type: {0}'.format(str(type(child)))) replacement_map_by_child[child] = tmp_component_map component_map.update(tmp_component_map) - logger.debug('creating linking cons linking the children of {0}'.format(str(block))) + logger.debug( + 'creating linking cons linking the children of {0}'.format(str(block)) + ) for edge in self.edges_between_children: logger.debug('adding linking constraint for edge {0}'.format(str(edge))) if edge.node1.comp is not edge.node2.comp: - raise DecompositionError('Edge {0} node1.comp is not node2.comp'.format(edge)) + raise DecompositionError( + 'Edge {0} node1.comp is not node2.comp'.format(edge) + ) if edge.node1.comp not in component_map: - logger.warning('Edge {0} node {1} is not in the component map'.format(str(edge), str(edge.node1))) + logger.warning( + 'Edge {0} node {1} is not in the component map'.format( + str(edge), str(edge.node1) + ) + ) all_children = list(self.children) assert len(all_children) == 2 child0 = all_children[0] @@ -271,12 +308,14 @@ def log(self, prefix=''): if isinstance(_child, _Tree): _child.log(prefix=prefix + ' ') else: - logger.debug(prefix + ' Leaf: # NNZ: {0}'.format(_child.number_of_edges())) + logger.debug( + prefix + ' Leaf: # NNZ: {0}'.format(_child.number_of_edges()) + ) def _is_dominated(ndx, num_cuts, balance, num_cuts_array, balance_array): - cut_diff = ((num_cuts - num_cuts_array) >= 0) - balance_diff = ((abs(balance - 0.5) - abs(balance_array - 0.5)) >= 0) + cut_diff = (num_cuts - num_cuts_array) >= 0 + balance_diff = (abs(balance - 0.5) - abs(balance_array - 0.5)) >= 0 cut_diff[ndx] = False balance_diff[ndx] = False return np.any(cut_diff & balance_diff) @@ -311,15 +350,28 @@ def choose_metis_partition(graph, max_size_diff_trials, seed_trials): seed_selected: float """ if not metis_available: - raise ImportError('Cannot perform graph partitioning without metis. Please install metis (including the python bindings).') + raise ImportError( + 'Cannot perform graph partitioning without metis. Please install metis (including the python bindings).' + ) cut_list = list() for _max_size_diff in max_size_diff_trials: for _seed in seed_trials: if _seed is None: - edgecuts, parts = metis.part_graph(_networkx_to_adjacency_list(graph), nparts=2, ubvec=[1 + _max_size_diff]) + edgecuts, parts = metis.part_graph( + _networkx_to_adjacency_list(graph), + nparts=2, + ubvec=[1 + _max_size_diff], + ) else: - edgecuts, parts = metis.part_graph(_networkx_to_adjacency_list(graph), nparts=2, ubvec=[1 + _max_size_diff], seed=_seed) - cut_list.append((edgecuts, sum(parts)/graph.number_of_nodes(), _max_size_diff, _seed)) + edgecuts, parts = metis.part_graph( + _networkx_to_adjacency_list(graph), + nparts=2, + ubvec=[1 + _max_size_diff], + seed=_seed, + ) + cut_list.append( + (edgecuts, sum(parts) / graph.number_of_nodes(), _max_size_diff, _seed) + ) cut_list.sort(key=lambda i: i[0]) ############################ @@ -352,7 +404,9 @@ def evaluate_partition(original_graph, tree): tree: _Tree """ original_graph_nnz = original_graph.number_of_edges() - original_graph_n_vars_to_tighten = len(collect_vars_to_tighten_from_graph(graph=original_graph)) + original_graph_n_vars_to_tighten = len( + collect_vars_to_tighten_from_graph(graph=original_graph) + ) original_obbt_nnz = original_graph_nnz * original_graph_n_vars_to_tighten tree_obbt_nnz = 0 @@ -370,10 +424,13 @@ def evaluate_partition(original_graph, tree): return partitioning_ratio -def _refine_partition(graph: nx.Graph, model: _BlockData, - removed_edges: Sequence[_Edge], - graph_a_nodes: MutableSet[_Node], - graph_b_nodes: MutableSet[_Node]): +def _refine_partition( + graph: nx.Graph, + model: _BlockData, + removed_edges: Sequence[_Edge], + graph_a_nodes: MutableSet[_Node], + graph_b_nodes: MutableSet[_Node], +): con_count = defaultdict(int) for edge in removed_edges: n1, n2 = edge.node1, edge.node2 @@ -389,9 +446,11 @@ def _refine_partition(graph: nx.Graph, model: _BlockData, new_body = flatten_expr(c.body) if type(new_body) is not numeric_expr.SumExpression: - logger.info(f'Constraint {str(c)} is contributing to {count} removed ' - f'edges, but we cannot split the constraint because the ' - f'body is not a SumExpression.') + logger.info( + f'Constraint {str(c)} is contributing to {count} removed ' + f'edges, but we cannot split the constraint because the ' + f'body is not a SumExpression.' + ) continue graph_a_args = list() @@ -418,10 +477,12 @@ def _refine_partition(graph: nx.Graph, model: _BlockData, graph_a_args.append(arg) if not correct_structure: - logger.info(f'Constriant {str(c)} is contributing to {count} removed ' - f'edges, but we cannot split the constraint because some of ' - f'the terms in the SumExpression contain variables from both ' - f'partitions.') + logger.info( + f'Constriant {str(c)} is contributing to {count} removed ' + f'edges, but we cannot split the constraint because some of ' + f'the terms in the SumExpression contain variables from both ' + f'partitions.' + ) continue # update the model @@ -492,12 +553,23 @@ def split_metis(graph, model): tree: _Tree """ if not metis_available: - raise ImportError('Cannot perform graph partitioning without metis. Please install metis (including the python bindings).') - max_size_diff, seed = choose_metis_partition(graph, max_size_diff_trials=[0.15], seed_trials=list(range(10))) + raise ImportError( + 'Cannot perform graph partitioning without metis. Please install metis (including the python bindings).' + ) + max_size_diff, seed = choose_metis_partition( + graph, max_size_diff_trials=[0.15], seed_trials=list(range(10)) + ) if seed is None: - edgecuts, parts = metis.part_graph(_networkx_to_adjacency_list(graph), nparts=2, ubvec=[1 + max_size_diff]) + edgecuts, parts = metis.part_graph( + _networkx_to_adjacency_list(graph), nparts=2, ubvec=[1 + max_size_diff] + ) else: - edgecuts, parts = metis.part_graph(_networkx_to_adjacency_list(graph), nparts=2, ubvec=[1 + max_size_diff], seed=seed) + edgecuts, parts = metis.part_graph( + _networkx_to_adjacency_list(graph), + nparts=2, + ubvec=[1 + max_size_diff], + seed=seed, + ) graph_a_nodes = OrderedSet() graph_b_nodes = OrderedSet() @@ -522,10 +594,13 @@ def split_metis(graph, model): else: removed_edges.append(_Edge(n1, n2)) - removed_edges = _refine_partition(graph=graph, model=model, - removed_edges=removed_edges, - graph_a_nodes=graph_a_nodes, - graph_b_nodes=graph_b_nodes) + removed_edges = _refine_partition( + graph=graph, + model=model, + removed_edges=removed_edges, + graph_a_nodes=graph_a_nodes, + graph_b_nodes=graph_b_nodes, + ) graph_a_edges = list() graph_b_edges = list() @@ -571,8 +646,9 @@ def split_metis(graph, model): graph_a.add_edges_from(graph_a_edges) graph_b.add_edges_from(graph_b_edges) - if ((graph_a.number_of_nodes() >= 0.99 * graph.number_of_nodes()) or - (graph_b.number_of_nodes() >= 0.99 * graph.number_of_nodes())): + if (graph_a.number_of_nodes() >= 0.99 * graph.number_of_nodes()) or ( + graph_b.number_of_nodes() >= 0.99 * graph.number_of_nodes() + ): raise DecompositionError('Failed to partition graph') tree = _Tree(children=[graph_a, graph_b], edges_between_children=linking_edges) @@ -595,7 +671,9 @@ def convert_pyomo_model_to_bipartite_graph(m: _BlockData): graph = networkx.Graph() var_map = pe.ComponentMap() - for v in nonrelaxation_component_data_objects(m, pe.Var, sort=True, descend_into=True): + for v in nonrelaxation_component_data_objects( + m, pe.Var, sort=True, descend_into=True + ): if v.fixed: continue var_map[v] = _VarNode(v) @@ -603,11 +681,13 @@ def convert_pyomo_model_to_bipartite_graph(m: _BlockData): for b in relaxation_data_objects(m, descend_into=True, active=True, sort=True): node2 = _RelNode(b) - for v in (list(b.get_rhs_vars()) + [b.get_aux_var()]): + for v in list(b.get_rhs_vars()) + [b.get_aux_var()]: node1 = var_map[v] graph.add_edge(node1, node2) - for c in nonrelaxation_component_data_objects(m, pe.Constraint, active=True, sort=True, descend_into=True): + for c in nonrelaxation_component_data_objects( + m, pe.Constraint, active=True, sort=True, descend_into=True + ): node2 = _ConNode(c) for v in identify_variables(c.body, include_fixed=False): node1 = var_map[v] @@ -670,13 +750,20 @@ def build_pyomo_model_from_graph(graph, block): for c_name, c in zip(con_names, cons): if c.comp.equality: - block.cons[c_name] = (replace_expressions(c.comp.body, substitution_map=var_map, - remove_named_expressions=True) == c.comp.lower) + block.cons[c_name] = ( + replace_expressions( + c.comp.body, substitution_map=var_map, remove_named_expressions=True + ) + == c.comp.lower + ) else: - block.cons[c_name] = (pe.inequality(lower=c.comp.lower, - body=replace_expressions(c.comp.body, substitution_map=var_map, - remove_named_expressions=True), - upper=c.comp.upper)) + block.cons[c_name] = pe.inequality( + lower=c.comp.lower, + body=replace_expressions( + c.comp.body, substitution_map=var_map, remove_named_expressions=True + ), + upper=c.comp.upper, + ) component_map[c.comp] = block.cons[c_name] for r_name, r in zip(rel_names, rels): @@ -710,14 +797,21 @@ class DecompositionStatus(enum.Enum): problem_too_small = 3 # the model could not be decomposed at all because the number of jacobian nonzeros in the original problem was less than max_leaf_nnz -def compute_partition_ratio(original_model: _BlockData, decomposed_model: TreeBlockData): +def compute_partition_ratio( + original_model: _BlockData, decomposed_model: TreeBlockData +): graph = convert_pyomo_model_to_bipartite_graph(original_model) - pr_numerator = graph.number_of_edges() * len(collect_vars_to_tighten(original_model)) + pr_numerator = graph.number_of_edges() * len( + collect_vars_to_tighten(original_model) + ) pr_denominator = 0 vars_to_tighten_by_block = collect_vars_to_tighten_by_block(decomposed_model, 'dbt') for block, vars_to_tighten in vars_to_tighten_by_block.items(): - pr_denominator += len(vars_to_tighten) * convert_pyomo_model_to_bipartite_graph(block).number_of_edges() + pr_denominator += ( + len(vars_to_tighten) + * convert_pyomo_model_to_bipartite_graph(block).number_of_edges() + ) pr = pr_numerator / pr_denominator return pr @@ -748,27 +842,42 @@ def _eliminate_mutable_params(model): for p in nonrelaxation_component_data_objects(model, pe.Param, descend_into=True): sub_map[id(p)] = p.value - for c in nonrelaxation_component_data_objects(model, pe.Constraint, active=True, descend_into=True): + for c in nonrelaxation_component_data_objects( + model, pe.Constraint, active=True, descend_into=True + ): if c.lower is None: new_lower = None else: - new_lower = replace_expressions(c.lower, sub_map, - descend_into_named_expressions=True, - remove_named_expressions=True) - new_body = replace_expressions(c.body, sub_map, - descend_into_named_expressions=True, - remove_named_expressions=True) + new_lower = replace_expressions( + c.lower, + sub_map, + descend_into_named_expressions=True, + remove_named_expressions=True, + ) + new_body = replace_expressions( + c.body, + sub_map, + descend_into_named_expressions=True, + remove_named_expressions=True, + ) if c.upper is None: new_upper = None else: - new_upper = replace_expressions(c.upper, sub_map, - descend_into_named_expressions=True, - remove_named_expressions=True) + new_upper = replace_expressions( + c.upper, + sub_map, + descend_into_named_expressions=True, + remove_named_expressions=True, + ) c.set_value((new_lower, new_body, new_upper)) -def _decompose_model(model: _BlockData, max_leaf_nnz: Optional[int] = None, - min_partition_ratio: float = 1.25, limit_num_stages: bool = True): +def _decompose_model( + model: _BlockData, + max_leaf_nnz: Optional[int] = None, + min_partition_ratio: float = 1.25, + limit_num_stages: bool = True, +): """ Parameters ---------- @@ -823,8 +932,11 @@ def _decompose_model(model: _BlockData, max_leaf_nnz: Optional[int] = None, logger.debug('done building pyomo model from graph') else: root_tree, partitioning_ratio = split_metis(graph=graph, model=model) - logger.debug('partitioned original tree; partitioning ratio: {ratio}'.format( - ratio=partitioning_ratio)) + logger.debug( + 'partitioned original tree; partitioning ratio: {ratio}'.format( + ratio=partitioning_ratio + ) + ) if partitioning_ratio < min_partition_ratio: logger.debug('obtained bad partitioning ratio; abandoning partition') new_model = TreeBlock(concrete=True) @@ -839,9 +951,12 @@ def _decompose_model(model: _BlockData, max_leaf_nnz: Optional[int] = None, needs_split = list() for child in parent.children: logger.debug( - 'number of NNZ in child: {0}'.format(child.number_of_edges())) - if child.number_of_edges() > max_leaf_nnz and num_cons_in_graph( - child) > 1: + 'number of NNZ in child: {0}'.format(child.number_of_edges()) + ) + if ( + child.number_of_edges() > max_leaf_nnz + and num_cons_in_graph(child) > 1 + ): needs_split.append((child, parent, 1)) while len(needs_split) > 0: @@ -849,34 +964,49 @@ def _decompose_model(model: _BlockData, max_leaf_nnz: Optional[int] = None, _graph, _parent, _stage = needs_split.pop() try: if _stage + 1 >= max_stages: - logger.debug(f'stage {_stage}: not partitiong graph with ' - f'{_graph.number_of_edges()} NNZ due to the max ' - f'stages rule;') + logger.debug( + f'stage {_stage}: not partitiong graph with ' + f'{_graph.number_of_edges()} NNZ due to the max ' + f'stages rule;' + ) continue - logger.debug(f'stage {_stage}: partitioning graph with ' - f'{_graph.number_of_edges()} NNZ') - sub_tree, partitioning_ratio = split_metis(graph=_graph, - model=model) logger.debug( - 'partitioning ratio: {ratio}'.format(ratio=partitioning_ratio)) + f'stage {_stage}: partitioning graph with ' + f'{_graph.number_of_edges()} NNZ' + ) + sub_tree, partitioning_ratio = split_metis( + graph=_graph, model=model + ) + logger.debug( + 'partitioning ratio: {ratio}'.format(ratio=partitioning_ratio) + ) if partitioning_ratio > min_partition_ratio: logger.debug('partitioned {0}'.format(str(_graph))) _parent.children.discard(_graph) _parent.children.add(sub_tree) for child in sub_tree.children: - logger.debug('number of NNZ in child: {0}'.format( - child.number_of_edges())) - if (child.number_of_edges() > max_leaf_nnz - and num_cons_in_graph(child) > 1): + logger.debug( + 'number of NNZ in child: {0}'.format( + child.number_of_edges() + ) + ) + if ( + child.number_of_edges() > max_leaf_nnz + and num_cons_in_graph(child) > 1 + ): needs_split.append((child, sub_tree, _stage + 1)) else: logger.debug( - 'obtained bad partitioning ratio; abandoning partition') + 'obtained bad partitioning ratio; abandoning partition' + ) except DecompositionError: termination_reason = DecompositionStatus.error - logger.error('failed to partition graph with {0} NNZ'.format( - _graph.number_of_edges())) + logger.error( + 'failed to partition graph with {0} NNZ'.format( + _graph.number_of_edges() + ) + ) logger.debug('Tree Info:') root_tree.log() @@ -889,9 +1019,11 @@ def _decompose_model(model: _BlockData, max_leaf_nnz: Optional[int] = None, if obj is not None: var_map = {id(k): v for k, v in component_map.items()} new_model.objective = pe.Objective( - expr=replace_expressions(obj.expr, substitution_map=var_map, - remove_named_expressions=True), - sense=obj.sense) + expr=replace_expressions( + obj.expr, substitution_map=var_map, remove_named_expressions=True + ), + sense=obj.sense, + ) logger.debug('done adding objective to new model') else: logger.debug('No objective was found to add to the new model') @@ -899,8 +1031,12 @@ def _decompose_model(model: _BlockData, max_leaf_nnz: Optional[int] = None, return new_model, component_map, termination_reason -def decompose_model(model: _BlockData, max_leaf_nnz: Optional[int] = None, - min_partition_ratio: float = 1.25, limit_num_stages: bool = True): +def decompose_model( + model: _BlockData, + max_leaf_nnz: Optional[int] = None, + min_partition_ratio: float = 1.25, + limit_num_stages: bool = True, +): """ Parameters ---------- @@ -925,27 +1061,42 @@ def decompose_model(model: _BlockData, max_leaf_nnz: Optional[int] = None, An enum member from DecompositionStatus """ # we have to clone the model because we modify it in _refine_partition - all_comps = list(ComponentSet( - nonrelaxation_component_data_objects(model, pe.Var, descend_into=True))) - all_comps.extend(ComponentSet( - nonrelaxation_component_data_objects(model, pe.Constraint, active=True, - descend_into=True))) + all_comps = list( + ComponentSet( + nonrelaxation_component_data_objects(model, pe.Var, descend_into=True) + ) + ) + all_comps.extend( + ComponentSet( + nonrelaxation_component_data_objects( + model, pe.Constraint, active=True, descend_into=True + ) + ) + ) all_comps.extend(relaxation_data_objects(model, descend_into=True, active=True)) - all_comps.extend(ComponentSet( - nonrelaxation_component_data_objects(model, pe.Objective, active=True, - descend_into=True))) + all_comps.extend( + ComponentSet( + nonrelaxation_component_data_objects( + model, pe.Objective, active=True, descend_into=True + ) + ) + ) tmp_name = unique_component_name(model, 'all_comps') setattr(model, tmp_name, all_comps) new_model = model.clone() - old_to_new_comps_map = pe.ComponentMap(zip(getattr(model, tmp_name), - getattr(new_model, tmp_name))) + old_to_new_comps_map = pe.ComponentMap( + zip(getattr(model, tmp_name), getattr(new_model, tmp_name)) + ) delattr(model, tmp_name) delattr(new_model, tmp_name) model = new_model - tmp = _decompose_model(model, max_leaf_nnz=max_leaf_nnz, - min_partition_ratio=min_partition_ratio, - limit_num_stages=limit_num_stages) + tmp = _decompose_model( + model, + max_leaf_nnz=max_leaf_nnz, + min_partition_ratio=min_partition_ratio, + limit_num_stages=limit_num_stages, + ) tree_model, component_map, termination_reason = tmp for orig_comp, clone_comp in list(old_to_new_comps_map.items()): @@ -961,9 +1112,17 @@ def collect_vars_to_tighten_from_graph(graph): for n in graph.nodes(): if n.is_rel(): rel: BaseRelaxationData = n.comp - if rel.is_rhs_convex() and rel.relaxation_side == RelaxationSide.UNDER and not rel.use_linear_relaxation: + if ( + rel.is_rhs_convex() + and rel.relaxation_side == RelaxationSide.UNDER + and not rel.use_linear_relaxation + ): continue - if rel.is_rhs_concave() and rel.relaxation_side == RelaxationSide.OVER and not rel.use_linear_relaxation: + if ( + rel.is_rhs_concave() + and rel.relaxation_side == RelaxationSide.OVER + and not rel.use_linear_relaxation + ): continue vars_to_tighten.update(rel.get_rhs_vars()) elif n.is_var(): @@ -1021,7 +1180,9 @@ def collect_vars_to_tighten_by_block(m, method): all_vars_to_account_for.discard(v) if len(all_vars_to_account_for) != 0: - raise RuntimeError('There are variables that need tightened that are unaccounted for!') + raise RuntimeError( + 'There are variables that need tightened that are unaccounted for!' + ) return vars_to_tighten_by_block @@ -1043,29 +1204,30 @@ class DBTInfo(object): ---------- num_coupling_vars_to_tighten: int The total number of coupling variables that need tightened. Note that this includes - coupling variables that get filtered. If you subtract num_coupling_vars_attempted - and num_coupling_vars_filtered from num_coupling_vars_to_tighten, you should get + coupling variables that get filtered. If you subtract num_coupling_vars_attempted + and num_coupling_vars_filtered from num_coupling_vars_to_tighten, you should get the number of coupling variables that were not tightened due to a time limit. num_coupling_vars_attempted: int The number of coupling variables for which tightening was attempted. num_coupling_vars_successful: int - The number of coupling variables for which tightening was attempted and the solver + The number of coupling variables for which tightening was attempted and the solver terminated optimally. num_coupling_vars_filtered: int The number of coupling vars that did not need to be tightened (identified by filtering). num_vars_to_tighten: int - The total number of nonlinear and discrete variables that need tightened. Note that - this includes variables that get filtered. If you subtract num_vars_attempted and - num_vars_filtered from num_vars_to_tighten, you should get the number of nonlinear + The total number of nonlinear and discrete variables that need tightened. Note that + this includes variables that get filtered. If you subtract num_vars_attempted and + num_vars_filtered from num_vars_to_tighten, you should get the number of nonlinear and discrete variables that were not tightened due to a time limit. num_vars_attempted: int The number of variables for which tightening was attempted. num_vars_successful: int - The number of variables for which tightening was attempted and the solver + The number of variables for which tightening was attempted and the solver terminated optimally. num_vars_filtered: int The number of vars that did not need to be tightened (identified by filtering). """ + def __init__(self): self.num_coupling_vars_to_tighten = None self.num_coupling_vars_attempted = None @@ -1088,7 +1250,14 @@ def __str__(self): return s -def _update_var_bounds(varlist, new_lower_bounds, new_upper_bounds, feasibility_tol, safety_tol, max_acceptable_bound): +def _update_var_bounds( + varlist, + new_lower_bounds, + new_upper_bounds, + feasibility_tol, + safety_tol, + max_acceptable_bound, +): for ndx, v in enumerate(varlist): new_lb = new_lower_bounds[ndx] new_ub = new_upper_bounds[ndx] @@ -1115,11 +1284,16 @@ def _update_var_bounds(varlist, new_lower_bounds, new_upper_bounds, feasibility_ new_ub = math.inf if new_lb > new_ub: - msg = 'variable ub is less than lb; var: {0}; lb: {1}; ub: {2}'.format(str(v), new_lb, new_ub) + msg = 'variable ub is less than lb; var: {0}; lb: {1}; ub: {2}'.format( + str(v), new_lb, new_ub + ) if new_lb > new_ub + feasibility_tol: raise ValueError(msg) else: - logger.warning(msg + '; decreasing lb and increasing ub by {0}'.format(feasibility_tol)) + logger.warning( + msg + + '; decreasing lb and increasing ub by {0}'.format(feasibility_tol) + ) warnings.warn(msg) new_lb -= feasibility_tol new_ub += feasibility_tol @@ -1135,11 +1309,21 @@ def _update_var_bounds(varlist, new_lower_bounds, new_upper_bounds, feasibility_ v.setub(new_ub) -def perform_dbt(relaxation, solver, obbt_method=OBBTMethod.DECOMPOSED, - filter_method=FilterMethod.AGGRESSIVE, time_limit=math.inf, - objective_bound=None, with_progress_bar=False, parallel=False, - vars_to_tighten_by_block=None, feasibility_tol=0, - safety_tol=0, max_acceptable_bound=math.inf, update_relaxations_between_stages=True): +def perform_dbt( + relaxation, + solver, + obbt_method=OBBTMethod.DECOMPOSED, + filter_method=FilterMethod.AGGRESSIVE, + time_limit=math.inf, + objective_bound=None, + with_progress_bar=False, + parallel=False, + vars_to_tighten_by_block=None, + feasibility_tol=0, + safety_tol=0, + max_acceptable_bound=math.inf, + update_relaxations_between_stages=True, +): """This function performs optimization-based bounds tightening (OBBT) with a decomposition scheme. Parameters @@ -1197,8 +1381,8 @@ def perform_dbt(relaxation, solver, obbt_method=OBBTMethod.DECOMPOSED, account for numerical error in the solution of the OBBT problems and to avoid cutting off valid portions of the feasible region. max_acceptable_bound: float - If the upper bound computed for a variable is larger than max_acceptable_bound, then the - computed bound will be rejected. If the lower bound computed for a variable is less than + If the upper bound computed for a variable is larger than max_acceptable_bound, then the + computed bound will be rejected. If the lower bound computed for a variable is less than -max_acceptable_bound, then the computed bound will be rejected. update_relaxations_between_stages: bool This is meant for unit testing only and should not be modified @@ -1209,7 +1393,7 @@ def perform_dbt(relaxation, solver, obbt_method=OBBTMethod.DECOMPOSED, """ t0 = time.time() - + if not isinstance(relaxation, TreeBlockData): raise ValueError('relaxation must be an instance of dbt.decomp.TreeBlockData.') if obbt_method not in OBBTMethod: @@ -1265,8 +1449,13 @@ def perform_dbt(relaxation, solver, obbt_method=OBBTMethod.DECOMPOSED, all_vars_to_tighten.update(block_vars_to_tighten) if filter_method == FilterMethod.AGGRESSIVE: logger.debug('starting full space filter') - res = aggressive_filter(candidate_variables=all_vars_to_tighten, relaxation=relaxation, - solver=solver, tolerance=1e-4, objective_bound=objective_bound) + res = aggressive_filter( + candidate_variables=all_vars_to_tighten, + relaxation=relaxation, + solver=solver, + tolerance=1e-4, + objective_bound=objective_bound, + ) full_space_lb_vars, full_space_ub_vars = res logger.debug('finished full space filter') else: @@ -1275,21 +1464,29 @@ def perform_dbt(relaxation, solver, obbt_method=OBBTMethod.DECOMPOSED, else: full_space_lb_vars = None full_space_ub_vars = None - + for stage in range(num_stages): logger.info(f'Performing DBT on stage {stage+1} of {num_stages}') if time.time() - t0 >= time_limit: break - + stage_blocks = list(relaxation.stage_blocks(stage)) - logger.debug('DBT stage {0} of {1} with {1} blocks'.format(stage, num_stages, len(stage_blocks))) + logger.debug( + 'DBT stage {0} of {1} with {1} blocks'.format( + stage, num_stages, len(stage_blocks) + ) + ) for block_ndx, block in enumerate(stage_blocks): - logger.info(f'performing DBT on block {block_ndx+1} of {len(stage_blocks)} in stage {stage+1}') + logger.info( + f'performing DBT on block {block_ndx+1} of {len(stage_blocks)} in stage {stage+1}' + ) if time.time() - t0 >= time_limit: break - if obbt_method in {OBBTMethod.LEAVES, OBBTMethod.FULL_SPACE} and (not block.is_leaf()): + if obbt_method in {OBBTMethod.LEAVES, OBBTMethod.FULL_SPACE} and ( + not block.is_leaf() + ): continue if obbt_method == OBBTMethod.FULL_SPACE: block_to_tighten_with = relaxation @@ -1300,49 +1497,84 @@ def perform_dbt(relaxation, solver, obbt_method=OBBTMethod.DECOMPOSED, _ub = objective_bound else: _ub = None - + vars_to_tighten = vars_to_tighten_by_block[block] if filter_method == FilterMethod.AGGRESSIVE: logger.debug('starting filter') if obbt_method == OBBTMethod.FULL_SPACE: - lb_vars = ComponentSet([v for v in vars_to_tighten if v in full_space_lb_vars]) - ub_vars = ComponentSet([v for v in vars_to_tighten if v in full_space_ub_vars]) + lb_vars = ComponentSet( + [v for v in vars_to_tighten if v in full_space_lb_vars] + ) + ub_vars = ComponentSet( + [v for v in vars_to_tighten if v in full_space_ub_vars] + ) else: - res = aggressive_filter(candidate_variables=vars_to_tighten, relaxation=block_to_tighten_with, - solver=solver, tolerance=1e-4, objective_bound=_ub) + res = aggressive_filter( + candidate_variables=vars_to_tighten, + relaxation=block_to_tighten_with, + solver=solver, + tolerance=1e-4, + objective_bound=_ub, + ) lb_vars, ub_vars = res if block.is_leaf(): - dbt_info.num_vars_filtered += 2*len(vars_to_tighten) - len(lb_vars) - len(ub_vars) + dbt_info.num_vars_filtered += ( + 2 * len(vars_to_tighten) - len(lb_vars) - len(ub_vars) + ) else: - dbt_info.num_coupling_vars_filtered += 2*len(vars_to_tighten) - len(lb_vars) - len(ub_vars) + dbt_info.num_coupling_vars_filtered += ( + 2 * len(vars_to_tighten) - len(lb_vars) - len(ub_vars) + ) logger.debug('done filtering') else: lb_vars = list(vars_to_tighten) ub_vars = list(vars_to_tighten) - logger.debug(f'performing OBBT (LB) on variables {str([str(i) for i in lb_vars])}') - res = normal_obbt(block_to_tighten_with, solver=solver, varlist=lb_vars, - objective_bound=_ub, with_progress_bar=with_progress_bar, - direction='lbs', time_limit=(time_limit - (time.time() - t0)), - update_bounds=False, parallel=parallel, collect_obbt_info=True, - progress_bar_string=f'DBT LBs Stage {stage+1} of {num_stages} Block {block_ndx+1} of {len(stage_blocks)}') + logger.debug( + f'performing OBBT (LB) on variables {str([str(i) for i in lb_vars])}' + ) + res = normal_obbt( + block_to_tighten_with, + solver=solver, + varlist=lb_vars, + objective_bound=_ub, + with_progress_bar=with_progress_bar, + direction='lbs', + time_limit=(time_limit - (time.time() - t0)), + update_bounds=False, + parallel=parallel, + collect_obbt_info=True, + progress_bar_string=f'DBT LBs Stage {stage+1} of {num_stages} Block {block_ndx+1} of {len(stage_blocks)}', + ) lower, unused_upper, obbt_info = res if block.is_leaf(): dbt_info.num_vars_attempted += obbt_info.num_problems_attempted dbt_info.num_vars_successful += obbt_info.num_successful_problems else: dbt_info.num_coupling_vars_attempted += obbt_info.num_problems_attempted - dbt_info.num_coupling_vars_successful += obbt_info.num_successful_problems + dbt_info.num_coupling_vars_successful += ( + obbt_info.num_successful_problems + ) logger.debug('done tightening lbs') - logger.debug(f'performing OBBT (UB) on variables {str([str(i) for i in ub_vars])}') - res = normal_obbt(block_to_tighten_with, solver=solver, varlist=ub_vars, - objective_bound=_ub, with_progress_bar=with_progress_bar, - direction='ubs', time_limit=(time_limit - (time.time() - t0)), - update_bounds=False, parallel=parallel, collect_obbt_info=True, - progress_bar_string=f'DBT UBs Stage {stage+1} of {num_stages} Block {block_ndx+1} of {len(stage_blocks)}') + logger.debug( + f'performing OBBT (UB) on variables {str([str(i) for i in ub_vars])}' + ) + res = normal_obbt( + block_to_tighten_with, + solver=solver, + varlist=ub_vars, + objective_bound=_ub, + with_progress_bar=with_progress_bar, + direction='ubs', + time_limit=(time_limit - (time.time() - t0)), + update_bounds=False, + parallel=parallel, + collect_obbt_info=True, + progress_bar_string=f'DBT UBs Stage {stage+1} of {num_stages} Block {block_ndx+1} of {len(stage_blocks)}', + ) unused_lower, upper, obbt_info = res if block.is_leaf(): @@ -1350,15 +1582,27 @@ def perform_dbt(relaxation, solver, obbt_method=OBBTMethod.DECOMPOSED, dbt_info.num_vars_successful += obbt_info.num_successful_problems else: dbt_info.num_coupling_vars_attempted += obbt_info.num_problems_attempted - dbt_info.num_coupling_vars_successful += obbt_info.num_successful_problems - - _update_var_bounds(varlist=lb_vars, new_lower_bounds=lower, - new_upper_bounds=unused_upper, feasibility_tol=feasibility_tol, - safety_tol=safety_tol, max_acceptable_bound=max_acceptable_bound) - - _update_var_bounds(varlist=ub_vars, new_lower_bounds=unused_lower, - new_upper_bounds=upper, feasibility_tol=feasibility_tol, - safety_tol=safety_tol, max_acceptable_bound=max_acceptable_bound) + dbt_info.num_coupling_vars_successful += ( + obbt_info.num_successful_problems + ) + + _update_var_bounds( + varlist=lb_vars, + new_lower_bounds=lower, + new_upper_bounds=unused_upper, + feasibility_tol=feasibility_tol, + safety_tol=safety_tol, + max_acceptable_bound=max_acceptable_bound, + ) + + _update_var_bounds( + varlist=ub_vars, + new_lower_bounds=unused_lower, + new_upper_bounds=upper, + feasibility_tol=feasibility_tol, + safety_tol=safety_tol, + max_acceptable_bound=max_acceptable_bound, + ) if update_relaxations_between_stages: # this is needed to ensure consistency for parallel computing; this accounts @@ -1419,11 +1663,21 @@ def pop_integers(relaxed_binary_vars, relaxed_integer_vars): v.domain = pe.Integers -def perform_dbt_with_integers_relaxed(relaxation, solver, obbt_method=OBBTMethod.DECOMPOSED, - filter_method=FilterMethod.AGGRESSIVE, time_limit=math.inf, - objective_bound=None, with_progress_bar=False, parallel=False, - vars_to_tighten_by_block=None, feasibility_tol=0, - integer_tol=1e-2, safety_tol=0, max_acceptable_bound=math.inf): +def perform_dbt_with_integers_relaxed( + relaxation, + solver, + obbt_method=OBBTMethod.DECOMPOSED, + filter_method=FilterMethod.AGGRESSIVE, + time_limit=math.inf, + objective_bound=None, + with_progress_bar=False, + parallel=False, + vars_to_tighten_by_block=None, + feasibility_tol=0, + integer_tol=1e-2, + safety_tol=0, + max_acceptable_bound=math.inf, +): """ This function performs optimization-based bounds tightening (OBBT) with a decomposition scheme. However, all OBBT problems are solved with the binary and integer variables relaxed. @@ -1487,8 +1741,8 @@ def perform_dbt_with_integers_relaxed(relaxation, solver, obbt_method=OBBTMethod account for numerical error in the solution of the OBBT problems and to avoid cutting off valid portions of the feasible region. max_acceptable_bound: float - If the upper bound computed for a variable is larger than max_acceptable_bound, then the - computed bound will be rejected. If the lower bound computed for a variable is less than + If the upper bound computed for a variable is larger than max_acceptable_bound, then the + computed bound will be rejected. If the lower bound computed for a variable is less than -max_acceptable_bound, then the computed bound will be rejected. Returns @@ -1507,22 +1761,24 @@ def perform_dbt_with_integers_relaxed(relaxation, solver, obbt_method=OBBTMethod relaxed_binary_vars, relaxed_integer_vars = push_integers(relaxation) - dbt_info = perform_dbt(relaxation=relaxation, - solver=solver, - obbt_method=obbt_method, - filter_method=filter_method, - time_limit=time_limit, - objective_bound=objective_bound, - with_progress_bar=with_progress_bar, - parallel=parallel, - vars_to_tighten_by_block=vars_to_tighten_by_block, - feasibility_tol=feasibility_tol, - safety_tol=safety_tol, - max_acceptable_bound=max_acceptable_bound) + dbt_info = perform_dbt( + relaxation=relaxation, + solver=solver, + obbt_method=obbt_method, + filter_method=filter_method, + time_limit=time_limit, + objective_bound=objective_bound, + with_progress_bar=with_progress_bar, + parallel=parallel, + vars_to_tighten_by_block=vars_to_tighten_by_block, + feasibility_tol=feasibility_tol, + safety_tol=safety_tol, + max_acceptable_bound=max_acceptable_bound, + ) pop_integers(relaxed_binary_vars, relaxed_integer_vars) - for v in (list(relaxed_binary_vars) + list(relaxed_integer_vars)): + for v in list(relaxed_binary_vars) + list(relaxed_integer_vars): lb = v.lb ub = v.ub if lb is None: diff --git a/pyomo/contrib/coramin/domain_reduction/filters.py b/pyomo/contrib/coramin/domain_reduction/filters.py index 5cf0822ce89..53e6fc31b2f 100644 --- a/pyomo/contrib/coramin/domain_reduction/filters.py +++ b/pyomo/contrib/coramin/domain_reduction/filters.py @@ -12,11 +12,13 @@ logger = logging.getLogger(__name__) -def filter_variables_from_solution(candidate_variables_at_relaxation_solution, tolerance=1e-6): +def filter_variables_from_solution( + candidate_variables_at_relaxation_solution, tolerance=1e-6 +): """ - This function takes a set of candidate variables for OBBT and filters out - the variables that are at their bounds in the provided solution to the - relaxation. See + This function takes a set of candidate variables for OBBT and filters out + the variables that are at their bounds in the provided solution to the + relaxation. See Gleixner, Ambros M., et al. "Three enhancements for optimization-based bound tightening." Journal of Global @@ -35,8 +37,8 @@ def filter_variables_from_solution(candidate_variables_at_relaxation_solution, t Parameters ---------- candidate_variables_at_relaxation_solution: iterable of _GeneralVarData - This should be an iterable of the variables which are candidates - for OBBT. The values of the variables should be feasible for the + This should be an iterable of the variables which are candidates + for OBBT. The values of the variables should be feasible for the relaxation that would be used to perform OBBT on the variables. tolerance: float A float greater than or equal to zero. If the value of the variable @@ -71,11 +73,11 @@ def aggressive_filter( tolerance: float = 1e-6, objective_bound: Optional[float] = None, max_iter: int = 10, - improvement_threshold: int = 5 + improvement_threshold: int = 5, ): """ - This function takes a set of candidate variables for OBBT and filters out - the variables for which it does not make senese to perform OBBT on. See + This function takes a set of candidate variables for OBBT and filters out + the variables for which it does not make senese to perform OBBT on. See Gleixner, Ambros M., et al. "Three enhancements for optimization-based bound tightening." Journal of Global @@ -86,13 +88,13 @@ def aggressive_filter( minimizing x subject to that relaxation is guaranteed to result in an optimal solution of x* = xl. - This function solves a series of optimization problems to try to + This function solves a series of optimization problems to try to filter as many variables as possible. Parameters ---------- candidate_variables: iterable of _GeneralVarData - This should be an iterable of the variables which are candidates + This should be an iterable of the variables which are candidates for OBBT. relaxation: Block a convex relaxation @@ -143,7 +145,9 @@ def aggressive_filter( else: obj_coefs = [-1 for v in _set] obj_vars = list(_set) - relaxation.__filter_obj = pe.Objective(expr=LinearExpression(linear_coefs=obj_coefs, linear_vars=obj_vars)) + relaxation.__filter_obj = pe.Objective( + expr=LinearExpression(linear_coefs=obj_coefs, linear_vars=obj_vars) + ) if solver.is_persistent(): solver.set_objective(relaxation.__filter_obj) solver.config.load_solution = False @@ -183,11 +187,15 @@ def aggressive_filter( vars_to_maximize.add(v) _bt_cleanup( - model=relaxation, solver=solver, vardatalist=None, + model=relaxation, + solver=solver, + vardatalist=None, initial_var_values=initial_var_values, deactivated_objectives=deactivated_objectives, - orig_update_config=orig_update_config, orig_config=orig_config, - lower_bounds=None, upper_bounds=None + orig_update_config=orig_update_config, + orig_config=orig_config, + lower_bounds=None, + upper_bounds=None, ) return vars_to_minimize, vars_to_maximize diff --git a/pyomo/contrib/coramin/domain_reduction/obbt.py b/pyomo/contrib/coramin/domain_reduction/obbt.py index 77abafe0f02..066f5c568a7 100644 --- a/pyomo/contrib/coramin/domain_reduction/obbt.py +++ b/pyomo/contrib/coramin/domain_reduction/obbt.py @@ -12,8 +12,10 @@ import time from typing import Union, Sequence, Optional, List from pyomo.core.base.objective import ScalarObjective, _GeneralObjectiveData + try: import pyomo.contrib.coramin.utils.mpi_utils as mpiu + mpi_available = True except ImportError: mpi_available = False @@ -34,11 +36,15 @@ def __init__(self): def _bt_cleanup( - model, solver: Union[appsi.base.Solver, appsi.base.PersistentSolver], + model, + solver: Union[appsi.base.Solver, appsi.base.PersistentSolver], vardatalist: Optional[List[_GeneralVarData]], - initial_var_values, deactivated_objectives, orig_update_config, orig_config, + initial_var_values, + deactivated_objectives, + orig_update_config, + orig_config, lower_bounds: Optional[Sequence[float]] = None, - upper_bounds: Optional[Sequence[float]] = None + upper_bounds: Optional[Sequence[float]] = None, ): """ Cleanup the changes made to the model during bounds tightening. @@ -62,7 +68,9 @@ def _bt_cleanup( Only needed if you want to update the bounds of the variables. Should be in the same order as self.vars_to_tighten. """ - for v in model.component_data_objects(ctype=pyo.Var, active=None, sort=True, descend_into=True): + for v in model.component_data_objects( + ctype=pyo.Var, active=None, sort=True, descend_into=True + ): v.set_value(initial_var_values[v], skip_validation=True) if hasattr(model, '__objective_ineq'): @@ -94,32 +102,40 @@ def _bt_cleanup( solver.update_variables(vardatalist) if solver.is_persistent(): - solver.update_config.check_for_new_or_removed_constraints = \ + solver.update_config.check_for_new_or_removed_constraints = ( orig_update_config.check_for_new_or_removed_constraints - solver.update_config.check_for_new_or_removed_vars = \ + ) + solver.update_config.check_for_new_or_removed_vars = ( orig_update_config.check_for_new_or_removed_vars - solver.update_config.check_for_new_or_removed_params = \ + ) + solver.update_config.check_for_new_or_removed_params = ( orig_update_config.check_for_new_or_removed_params - solver.update_config.check_for_new_objective = \ + ) + solver.update_config.check_for_new_objective = ( orig_update_config.check_for_new_objective - solver.update_config.update_constraints = \ - orig_update_config.update_constraints - solver.update_config.update_vars = \ - orig_update_config.update_vars - solver.update_config.update_params = \ - orig_update_config.update_params - solver.update_config.update_named_expressions = \ + ) + solver.update_config.update_constraints = orig_update_config.update_constraints + solver.update_config.update_vars = orig_update_config.update_vars + solver.update_config.update_params = orig_update_config.update_params + solver.update_config.update_named_expressions = ( orig_update_config.update_named_expressions - solver.update_config.update_objective = \ - orig_update_config.update_objective - solver.update_config.treat_fixed_vars_as_params = \ + ) + solver.update_config.update_objective = orig_update_config.update_objective + solver.update_config.treat_fixed_vars_as_params = ( orig_update_config.treat_fixed_vars_as_params + ) solver.config.stream_solver = orig_config.stream_solver solver.config.load_solution = orig_config.load_solution -def _single_solve(v, model, solver: Union[appsi.base.Solver, appsi.base.PersistentSolver], lb_or_ub, obbt_info): +def _single_solve( + v, + model, + solver: Union[appsi.base.Solver, appsi.base.PersistentSolver], + lb_or_ub, + obbt_info, +): obbt_info.num_problems_attempted += 1 # solve for lower var bound if lb_or_ub == 'lb': @@ -133,7 +149,9 @@ def _single_solve(v, model, solver: Union[appsi.base.Solver, appsi.base.Persiste results = solver.solve(model) if results.termination_condition == appsi.base.TerminationCondition.optimal: obbt_info.num_successful_problems += 1 - if results.best_objective_bound is not None and math.isfinite(results.best_objective_bound): + if results.best_objective_bound is not None and math.isfinite( + results.best_objective_bound + ): new_bnd = results.best_objective_bound elif results.termination_condition == appsi.base.TerminationCondition.optimal: new_bnd = results.best_feasible_objective # assumes the problem is convex @@ -167,7 +185,16 @@ def _single_solve(v, model, solver: Union[appsi.base.Solver, appsi.base.Persiste return new_bnd -def _tighten_bnds(model, solver, vardatalist, lb_or_ub, obbt_info, with_progress_bar=False, time_limit=math.inf, progress_bar_string=None): +def _tighten_bnds( + model, + solver, + vardatalist, + lb_or_ub, + obbt_info, + with_progress_bar=False, + time_limit=math.inf, + progress_bar_string=None, +): """ Tighten the lower bounds of all variables in vardatalist (or self.vars_to_tighten if vardatalist is None). @@ -203,7 +230,9 @@ def _tighten_bnds(model, solver, vardatalist, lb_or_ub, obbt_info, with_progress tqdm_position = mpiu.MPI.COMM_WORLD.Get_rank() else: tqdm_position = 0 - for v in tqdm(vardatalist, ncols=100, desc=bnd_str, leave=False, position=tqdm_position): + for v in tqdm( + vardatalist, ncols=100, desc=bnd_str, leave=False, position=tqdm_position + ): if time.time() - t0 > time_limit: if lb_or_ub == 'lb': if v.lb is None: @@ -216,9 +245,13 @@ def _tighten_bnds(model, solver, vardatalist, lb_or_ub, obbt_info, with_progress else: new_bounds.append(pyo.value(v.ub)) else: - new_bnd = _single_solve(v=v, model=model, solver=solver, - lb_or_ub=lb_or_ub, - obbt_info=obbt_info) + new_bnd = _single_solve( + v=v, + model=model, + solver=solver, + lb_or_ub=lb_or_ub, + obbt_info=obbt_info, + ) new_bounds.append(new_bnd) else: for v in vardatalist: @@ -234,9 +267,13 @@ def _tighten_bnds(model, solver, vardatalist, lb_or_ub, obbt_info, with_progress else: new_bounds.append(pyo.value(v.ub)) else: - new_bnd = _single_solve(v=v, model=model, solver=solver, - lb_or_ub=lb_or_ub, - obbt_info=obbt_info) + new_bnd = _single_solve( + v=v, + model=model, + solver=solver, + lb_or_ub=lb_or_ub, + obbt_info=obbt_info, + ) new_bounds.append(new_bnd) return new_bounds @@ -287,11 +324,15 @@ def _bt_prep(model, solver, objective_bound=None): solver.set_instance(model) initial_var_values = ComponentMap() - for v in model.component_data_objects(ctype=pyo.Var, active=None, sort=True, descend_into=True): + for v in model.component_data_objects( + ctype=pyo.Var, active=None, sort=True, descend_into=True + ): initial_var_values[v] = v.value deactivated_objectives = list() - for obj in model.component_data_objects(pyo.Objective, active=True, sort=True, descend_into=True): + for obj in model.component_data_objects( + pyo.Objective, active=True, sort=True, descend_into=True + ): deactivated_objectives.append(obj) obj.deactivate() @@ -299,17 +340,22 @@ def _bt_prep(model, solver, objective_bound=None): # obj.expr <= objective_ub if objective_bound is not None and math.isfinite(objective_bound): if len(deactivated_objectives) != 1: - e = 'BoundsTightener: When providing objective_ub,' + \ - ' the model must have one and only one objective function.' + e = ( + 'BoundsTightener: When providing objective_ub,' + + ' the model must have one and only one objective function.' + ) logger.error(e) raise ValueError(e) original_obj = deactivated_objectives[0] if original_obj.sense == minimize: - model.__objective_ineq = \ - pyo.Constraint(expr=original_obj.expr <= objective_bound) + model.__objective_ineq = pyo.Constraint( + expr=original_obj.expr <= objective_bound + ) else: assert original_obj.sense == maximize - model.__objective_ineq = pyo.Constraint(expr=original_obj.expr >= objective_bound) + model.__objective_ineq = pyo.Constraint( + expr=original_obj.expr >= objective_bound + ) if solver.is_persistent(): solver.add_constraints([model.__objective_ineq]) @@ -354,7 +400,9 @@ def _build_vardatalist(model, varlist=None, warning_threshold=0): if not v.is_fixed(): if v.has_lb() and v.has_ub(): if v.ub - v.lb < warning_threshold: - e = 'Warning: Tightening a variable with ub - lb is less than {threshold}: {v}, lb: {lb}, ub: {ub}'.format(threshold=warning_threshold, v=v, lb=v.lb, ub=v.ub) + e = 'Warning: Tightening a variable with ub - lb is less than {threshold}: {v}, lb: {lb}, ub: {ub}'.format( + threshold=warning_threshold, v=v, lb=v.lb, ub=v.ub + ) logger.warning(e) warnings.warn(e) corrected_vardatalist.append(v) @@ -362,9 +410,20 @@ def _build_vardatalist(model, varlist=None, warning_threshold=0): return corrected_vardatalist -def perform_obbt(model, solver, varlist=None, objective_bound=None, update_bounds=True, with_progress_bar=False, - direction='both', time_limit=math.inf, parallel=True, collect_obbt_info=False, - warning_threshold=0, progress_bar_string=None): +def perform_obbt( + model, + solver, + varlist=None, + objective_bound=None, + update_bounds=True, + with_progress_bar=False, + direction='both', + time_limit=math.inf, + parallel=True, + collect_obbt_info=False, + warning_threshold=0, + progress_bar_string=None, +): """ Perform optimization-based bounds tighening on the variables in varlist subject to the constraints in model. @@ -410,9 +469,16 @@ def perform_obbt(model, solver, varlist=None, objective_bound=None, update_bound obbt_info.num_successful_problems = 0 t0 = time.time() - initial_var_values, deactivated_objectives, orig_update_config, orig_config = _bt_prep(model=model, solver=solver, objective_bound=objective_bound) - - vardata_list = _build_vardatalist(model=model, varlist=varlist, warning_threshold=warning_threshold) + ( + initial_var_values, + deactivated_objectives, + orig_update_config, + orig_config, + ) = _bt_prep(model=model, solver=solver, objective_bound=objective_bound) + + vardata_list = _build_vardatalist( + model=model, varlist=varlist, warning_threshold=warning_threshold + ) if mpi_available and parallel: mpi_interface = mpiu.MPIInterface() alloc_map = mpiu.MPIAllocationMap(mpi_interface, len(vardata_list)) @@ -423,13 +489,16 @@ def perform_obbt(model, solver, varlist=None, objective_bound=None, update_bound exc = None try: if direction in {'both', 'lbs'}: - local_lower_bounds = _tighten_bnds(model=model, solver=solver, - vardatalist=local_vardata_list, - lb_or_ub='lb', - obbt_info=obbt_info, - with_progress_bar=with_progress_bar, - time_limit=(time_limit - (time.time() - t0)), - progress_bar_string=progress_bar_string) + local_lower_bounds = _tighten_bnds( + model=model, + solver=solver, + vardatalist=local_vardata_list, + lb_or_ub='lb', + obbt_info=obbt_info, + with_progress_bar=with_progress_bar, + time_limit=(time_limit - (time.time() - t0)), + progress_bar_string=progress_bar_string, + ) else: local_lower_bounds = list() for v in local_vardata_list: @@ -438,13 +507,16 @@ def perform_obbt(model, solver, varlist=None, objective_bound=None, update_bound else: local_lower_bounds.append(pyo.value(v.lb)) if direction in {'both', 'ubs'}: - local_upper_bounds = _tighten_bnds(model=model, solver=solver, - vardatalist=local_vardata_list, - lb_or_ub='ub', - obbt_info=obbt_info, - with_progress_bar=with_progress_bar, - time_limit=(time_limit - (time.time() - t0)), - progress_bar_string=progress_bar_string) + local_upper_bounds = _tighten_bnds( + model=model, + solver=solver, + vardatalist=local_vardata_list, + lb_or_ub='ub', + obbt_info=obbt_info, + with_progress_bar=with_progress_bar, + time_limit=(time_limit - (time.time() - t0)), + progress_bar_string=progress_bar_string, + ) else: local_upper_bounds = list() for v in local_vardata_list: @@ -462,8 +534,12 @@ def perform_obbt(model, solver, varlist=None, objective_bound=None, update_bound if mpi_available and parallel: local_status = np.array([status], dtype='i') - global_status = np.array([0 for i in range(mpiu.MPI.COMM_WORLD.Get_size())], dtype='i') - mpiu.MPI.COMM_WORLD.Allgatherv([local_status, mpiu.MPI.INT], [global_status, mpiu.MPI.INT]) + global_status = np.array( + [0 for i in range(mpiu.MPI.COMM_WORLD.Get_size())], dtype='i' + ) + mpiu.MPI.COMM_WORLD.Allgatherv( + [local_status, mpiu.MPI.INT], [global_status, mpiu.MPI.INT] + ) if not np.all(global_status): messages = mpi_interface.comm.allgather(msg) msg = None @@ -471,7 +547,9 @@ def perform_obbt(model, solver, varlist=None, objective_bound=None, update_bound if m is not None: msg = m logger.error('An error was raised in one or more processes:\n' + msg) - raise mpiu.MPISyncError('An error was raised in one or more processes:\n' + msg) + raise mpiu.MPISyncError( + 'An error was raised in one or more processes:\n' + msg + ) else: if status != 1: logger.error('An error was raised during OBBT:\n' + msg) @@ -480,9 +558,15 @@ def perform_obbt(model, solver, varlist=None, objective_bound=None, update_bound if mpi_available and parallel: global_lower = alloc_map.global_list_float64(local_lower_bounds) global_upper = alloc_map.global_list_float64(local_upper_bounds) - obbt_info.total_num_problems = mpiu.MPI.COMM_WORLD.allreduce(obbt_info.total_num_problems) - obbt_info.num_problems_attempted = mpiu.MPI.COMM_WORLD.allreduce(obbt_info.num_problems_attempted) - obbt_info.num_successful_problems = mpiu.MPI.COMM_WORLD.allreduce(obbt_info.num_successful_problems) + obbt_info.total_num_problems = mpiu.MPI.COMM_WORLD.allreduce( + obbt_info.total_num_problems + ) + obbt_info.num_problems_attempted = mpiu.MPI.COMM_WORLD.allreduce( + obbt_info.num_problems_attempted + ) + obbt_info.num_successful_problems = mpiu.MPI.COMM_WORLD.allreduce( + obbt_info.num_successful_problems + ) else: global_lower = local_lower_bounds global_upper = local_upper_bounds @@ -509,11 +593,15 @@ def perform_obbt(model, solver, varlist=None, objective_bound=None, update_bound _lower_bounds = global_lower _upper_bounds = global_upper _bt_cleanup( - model=model, solver=solver, vardatalist=vardata_list, + model=model, + solver=solver, + vardatalist=vardata_list, initial_var_values=initial_var_values, deactivated_objectives=deactivated_objectives, - orig_update_config=orig_update_config, orig_config=orig_config, - lower_bounds=_lower_bounds, upper_bounds=_upper_bounds + orig_update_config=orig_update_config, + orig_config=orig_config, + lower_bounds=_lower_bounds, + upper_bounds=_upper_bounds, ) if collect_obbt_info: diff --git a/pyomo/contrib/coramin/domain_reduction/tests/test_dbt.py b/pyomo/contrib/coramin/domain_reduction/tests/test_dbt.py index e71e3ad6ef0..70cfbfd6159 100644 --- a/pyomo/contrib/coramin/domain_reduction/tests/test_dbt.py +++ b/pyomo/contrib/coramin/domain_reduction/tests/test_dbt.py @@ -1,6 +1,18 @@ -from pyomo.contrib.coramin.domain_reduction.dbt import TreeBlock, TreeBlockError, convert_pyomo_model_to_bipartite_graph, \ - _VarNode, _ConNode, _RelNode, split_metis, num_cons_in_graph, collect_vars_to_tighten_by_block, decompose_model, \ - perform_dbt, OBBTMethod, FilterMethod +from pyomo.contrib.coramin.domain_reduction.dbt import ( + TreeBlock, + TreeBlockError, + convert_pyomo_model_to_bipartite_graph, + _VarNode, + _ConNode, + _RelNode, + split_metis, + num_cons_in_graph, + collect_vars_to_tighten_by_block, + decompose_model, + perform_dbt, + OBBTMethod, + FilterMethod, +) from pyomo.common import unittest import pyomo.environ as pe from pyomo.contrib import coramin @@ -71,7 +83,9 @@ def test_tree_block(self): b.children[2].x = pe.Var() b.children[2].children['a'].x = pe.Var() b.children[2].children['b'].x = pe.Var() - self.assertEqual(len(list(b.component_data_objects(pe.Var, descend_into=True, sort=True))), 3) + self.assertEqual( + len(list(b.component_data_objects(pe.Var, descend_into=True, sort=True))), 3 + ) self.assertEqual(b.num_stages(), 3) with self.assertRaises(TreeBlockError): @@ -144,15 +158,42 @@ def test_convert_pyomo_model_to_bipartite_graph(self): self.assertIn(m.c2, graph_node_comps) self.assertIn(m.c3, graph_node_comps) graph_edge_comps = {(id(i.comp), id(j.comp)) for i, j in graph.edges()} - self.assertTrue(((id(m.x), id(m.c1)) in graph_edge_comps) or ((id(m.c1), id(m.x)) in graph_edge_comps)) - self.assertTrue(((id(m.y), id(m.c1)) in graph_edge_comps) or ((id(m.c1), id(m.y)) in graph_edge_comps)) - self.assertTrue(((id(m.z), id(m.c1)) in graph_edge_comps) or ((id(m.c1), id(m.z)) in graph_edge_comps)) - self.assertTrue(((id(m.x), id(m.c2)) in graph_edge_comps) or ((id(m.c2), id(m.x)) in graph_edge_comps)) - self.assertFalse(((id(m.y), id(m.c2)) in graph_edge_comps) or ((id(m.c2), id(m.y)) in graph_edge_comps)) - self.assertTrue(((id(m.z), id(m.c2)) in graph_edge_comps) or ((id(m.c2), id(m.z)) in graph_edge_comps)) - self.assertTrue(((id(m.x), id(m.c3)) in graph_edge_comps) or ((id(m.c3), id(m.x)) in graph_edge_comps)) - self.assertTrue(((id(m.y), id(m.c3)) in graph_edge_comps) or ((id(m.c3), id(m.y)) in graph_edge_comps)) - self.assertTrue(((id(m.z), id(m.c3)) in graph_edge_comps) or ((id(m.c3), id(m.z)) in graph_edge_comps)) + self.assertTrue( + ((id(m.x), id(m.c1)) in graph_edge_comps) + or ((id(m.c1), id(m.x)) in graph_edge_comps) + ) + self.assertTrue( + ((id(m.y), id(m.c1)) in graph_edge_comps) + or ((id(m.c1), id(m.y)) in graph_edge_comps) + ) + self.assertTrue( + ((id(m.z), id(m.c1)) in graph_edge_comps) + or ((id(m.c1), id(m.z)) in graph_edge_comps) + ) + self.assertTrue( + ((id(m.x), id(m.c2)) in graph_edge_comps) + or ((id(m.c2), id(m.x)) in graph_edge_comps) + ) + self.assertFalse( + ((id(m.y), id(m.c2)) in graph_edge_comps) + or ((id(m.c2), id(m.y)) in graph_edge_comps) + ) + self.assertTrue( + ((id(m.z), id(m.c2)) in graph_edge_comps) + or ((id(m.c2), id(m.z)) in graph_edge_comps) + ) + self.assertTrue( + ((id(m.x), id(m.c3)) in graph_edge_comps) + or ((id(m.c3), id(m.x)) in graph_edge_comps) + ) + self.assertTrue( + ((id(m.y), id(m.c3)) in graph_edge_comps) + or ((id(m.c3), id(m.y)) in graph_edge_comps) + ) + self.assertTrue( + ((id(m.z), id(m.c3)) in graph_edge_comps) + or ((id(m.c3), id(m.z)) in graph_edge_comps) + ) self.assertEqual(num_cons_in_graph(graph=graph, include_rels=True), 3) self.assertEqual(num_cons_in_graph(graph=graph, include_rels=False), 2) @@ -204,7 +245,7 @@ def test_split_metis(self): g.add_edge(v6, c2) tree, partitioning_ratio = split_metis(graph=g, model=m) - self.assertAlmostEqual(partitioning_ratio, 3*12/(14*1+6*2+6*2)) + self.assertAlmostEqual(partitioning_ratio, 3 * 12 / (14 * 1 + 6 * 2 + 6 * 2)) children = list(tree.children) self.assertEqual(len(children), 2) @@ -249,23 +290,32 @@ def test_split_metis(self): edges_between_children = list(tree.edges_between_children) self.assertEqual(len(edges_between_children), 1) edge = edges_between_children[0] - self.assertTrue((v4 is edge.node1 and v4_hat is edge.node2) or (v4 is edge.node2 and v4_hat is edge.node1)) + self.assertTrue( + (v4 is edge.node1 and v4_hat is edge.node2) + or (v4 is edge.node2 and v4_hat is edge.node1) + ) new_model = TreeBlock(concrete=True) component_map = tree.build_pyomo_model(block=new_model) - new_vars = list(coramin.relaxations.nonrelaxation_component_data_objects(new_model, - ctype=pe.Var, - descend_into=True, - sort=True)) - new_cons = list(coramin.relaxations.nonrelaxation_component_data_objects(new_model, - ctype=pe.Constraint, - active=True, - descend_into=True, - sort=True)) - new_rels = list(coramin.relaxations.relaxation_data_objects(new_model, - descend_into=True, - active=True, - sort=True)) + new_vars = list( + coramin.relaxations.nonrelaxation_component_data_objects( + new_model, ctype=pe.Var, descend_into=True, sort=True + ) + ) + new_cons = list( + coramin.relaxations.nonrelaxation_component_data_objects( + new_model, + ctype=pe.Constraint, + active=True, + descend_into=True, + sort=True, + ) + ) + new_rels = list( + coramin.relaxations.relaxation_data_objects( + new_model, descend_into=True, active=True, sort=True + ) + ) self.assertEqual(len(new_vars), 7) self.assertEqual(len(new_cons), 3) self.assertEqual(len(new_rels), 2) @@ -273,44 +323,55 @@ def test_split_metis(self): self.assertEqual(len(new_model.linking_constraints), 1) self.assertEqual(new_model.num_stages(), 2) - stage0_vars = list(new_model.component_data_objects(pe.Var, descend_into=False, sort=True)) - stage0_cons = list(new_model.component_data_objects(pe.Constraint, descend_into=False, sort=True, active=True)) - stage0_rels = list(coramin.relaxations.relaxation_data_objects(new_model, - descend_into=False, - active=True, - sort=True)) + stage0_vars = list( + new_model.component_data_objects(pe.Var, descend_into=False, sort=True) + ) + stage0_cons = list( + new_model.component_data_objects( + pe.Constraint, descend_into=False, sort=True, active=True + ) + ) + stage0_rels = list( + coramin.relaxations.relaxation_data_objects( + new_model, descend_into=False, active=True, sort=True + ) + ) self.assertEqual(len(stage0_vars), 0) self.assertEqual(len(stage0_cons), 1) self.assertEqual(len(stage0_rels), 0) block_a = new_model.children[0] block_b = new_model.children[1] - block_a_vars = ComponentSet(coramin.relaxations.nonrelaxation_component_data_objects(block_a, - ctype=pe.Var, - descend_into=True, - sort=True)) - block_b_vars = ComponentSet(coramin.relaxations.nonrelaxation_component_data_objects(block_b, - ctype=pe.Var, - descend_into=True, - sort=True)) - block_a_cons = ComponentSet(coramin.relaxations.nonrelaxation_component_data_objects(block_a, - ctype=pe.Constraint, - descend_into=True, - active=True, - sort=True)) - block_b_cons = ComponentSet(coramin.relaxations.nonrelaxation_component_data_objects(block_b, - ctype=pe.Constraint, - descend_into=True, - active=True, - sort=True)) - block_a_rels = ComponentSet(coramin.relaxations.relaxation_data_objects(block_a, - descend_into=True, - active=True, - sort=True)) - block_b_rels = ComponentSet(coramin.relaxations.relaxation_data_objects(block_b, - descend_into=True, - active=True, - sort=True)) + block_a_vars = ComponentSet( + coramin.relaxations.nonrelaxation_component_data_objects( + block_a, ctype=pe.Var, descend_into=True, sort=True + ) + ) + block_b_vars = ComponentSet( + coramin.relaxations.nonrelaxation_component_data_objects( + block_b, ctype=pe.Var, descend_into=True, sort=True + ) + ) + block_a_cons = ComponentSet( + coramin.relaxations.nonrelaxation_component_data_objects( + block_a, ctype=pe.Constraint, descend_into=True, active=True, sort=True + ) + ) + block_b_cons = ComponentSet( + coramin.relaxations.nonrelaxation_component_data_objects( + block_b, ctype=pe.Constraint, descend_into=True, active=True, sort=True + ) + ) + block_a_rels = ComponentSet( + coramin.relaxations.relaxation_data_objects( + block_a, descend_into=True, active=True, sort=True + ) + ) + block_b_rels = ComponentSet( + coramin.relaxations.relaxation_data_objects( + block_b, descend_into=True, active=True, sort=True + ) + ) if component_map[m.v1] not in block_a_vars: block_a, block_b = block_b, block_a block_a_vars, block_b_vars = block_b_vars, block_a_vars @@ -358,9 +419,13 @@ def test_split_metis(self): self.assertEqual(len(linking_con_vars), 2) self.assertIn(v4_a, linking_con_vars) self.assertIn(v4_b, linking_con_vars) - derivs = differentiate(expr=linking_con.body, mode=differentiate.Modes.reverse_symbolic) - self.assertTrue((derivs[v4_a] == 1 and derivs[v4_b] == -1) or - (derivs[v4_a] == -1 and derivs[v4_b] == 1)) + derivs = differentiate( + expr=linking_con.body, mode=differentiate.Modes.reverse_symbolic + ) + self.assertTrue( + (derivs[v4_a] == 1 and derivs[v4_b] == -1) + or (derivs[v4_a] == -1 and derivs[v4_b] == 1) + ) self.assertEqual(linking_con.lower, 0) self.assertEqual(linking_con.upper, 0) @@ -409,11 +474,13 @@ def test_num_cons(self): m.y = pe.Var() m.z = pe.Var() m.r = coramin.relaxations.PWUnivariateRelaxation() - m.r.build(x=m.x, - aux_var=m.y, - shape=coramin.utils.FunctionShape.CONVEX, - f_x_expr=pe.exp(m.x)) - m.c = pe.Constraint(expr=m.z == 2*m.x) + m.r.build( + x=m.x, + aux_var=m.y, + shape=coramin.utils.FunctionShape.CONVEX, + f_x_expr=pe.exp(m.x), + ) + m.c = pe.Constraint(expr=m.z == 2 * m.x) g = convert_pyomo_model_to_bipartite_graph(m) self.assertEqual(num_cons_in_graph(g, include_rels=False), 1) self.assertEqual(num_cons_in_graph(g), 2) @@ -435,34 +502,46 @@ def helper(self, case, min_partition_ratio, expected_termination): opt = pe.SolverFactory('ipopt') res = opt.solve(m, tee=False) - relaxed_m = coramin.relaxations.relax(m, - in_place=False, - use_fbbt=False, - fbbt_options={'deactivate_satisfied_constraints': True, - 'max_iter': 2}, - use_alpha_bb=False) - (decomposed_m, - component_map, - termination_reason) = decompose_model(model=relaxed_m, - max_leaf_nnz=1000, - min_partition_ratio=1.4, - limit_num_stages=True) + relaxed_m = coramin.relaxations.relax( + m, + in_place=False, + use_fbbt=False, + fbbt_options={'deactivate_satisfied_constraints': True, 'max_iter': 2}, + use_alpha_bb=False, + ) + (decomposed_m, component_map, termination_reason) = decompose_model( + model=relaxed_m, + max_leaf_nnz=1000, + min_partition_ratio=1.4, + limit_num_stages=True, + ) self.assertEqual(termination_reason, expected_termination) - if expected_termination == coramin.domain_reduction.dbt.DecompositionStatus.normal: + if ( + expected_termination + == coramin.domain_reduction.dbt.DecompositionStatus.normal + ): self.assertGreaterEqual(decomposed_m.num_stages(), 2) - for r in coramin.relaxations.relaxation_data_objects(block=relaxed_m, descend_into=True, - active=True, sort=True): + for r in coramin.relaxations.relaxation_data_objects( + block=relaxed_m, descend_into=True, active=True, sort=True + ): r.rebuild(build_nonlinear_constraint=True) - for r in coramin.relaxations.relaxation_data_objects(block=decomposed_m, descend_into=True, - active=True, sort=True): + for r in coramin.relaxations.relaxation_data_objects( + block=decomposed_m, descend_into=True, active=True, sort=True + ): r.rebuild(build_nonlinear_constraint=True) relaxed_res = opt.solve(relaxed_m, tee=False) decomposed_res = opt.solve(decomposed_m, tee=False) - self.assertEqual(res.solver.termination_condition, pe.TerminationCondition.optimal) - self.assertEqual(relaxed_res.solver.termination_condition, pe.TerminationCondition.optimal) - self.assertEqual(decomposed_res.solver.termination_condition, pe.TerminationCondition.optimal) + self.assertEqual( + res.solver.termination_condition, pe.TerminationCondition.optimal + ) + self.assertEqual( + relaxed_res.solver.termination_condition, pe.TerminationCondition.optimal + ) + self.assertEqual( + decomposed_res.solver.termination_condition, pe.TerminationCondition.optimal + ) obj = get_objective(m) relaxed_obj = get_objective(relaxed_m) decomposed_obj = get_objective(decomposed_m) @@ -474,33 +553,37 @@ def helper(self, case, min_partition_ratio, expected_termination): self.assertAlmostEqual(relaxed_rel_diff, 0, 5) self.assertAlmostEqual(decomposed_rel_diff, 0, 5) - relaxed_vars = list(coramin.relaxations.nonrelaxation_component_data_objects(relaxed_m, - pe.Var, - sort=True, - descend_into=True)) + relaxed_vars = list( + coramin.relaxations.nonrelaxation_component_data_objects( + relaxed_m, pe.Var, sort=True, descend_into=True + ) + ) relaxed_vars = [v for v in relaxed_vars if not v.fixed] - relaxed_cons = list(coramin.relaxations.nonrelaxation_component_data_objects(relaxed_m, - pe.Constraint, - active=True, - sort=True, - descend_into=True)) - relaxed_rels = list(coramin.relaxations.relaxation_data_objects(relaxed_m, - descend_into=True, - active=True, - sort=True)) - decomposed_vars = list(coramin.relaxations.nonrelaxation_component_data_objects(decomposed_m, - pe.Var, - sort=True, - descend_into=True)) - decomposed_cons = list(coramin.relaxations.nonrelaxation_component_data_objects(decomposed_m, - pe.Constraint, - active=True, - sort=True, - descend_into=True)) - decomposed_rels = list(coramin.relaxations.relaxation_data_objects(decomposed_m, - descend_into=True, - active=True, - sort=True)) + relaxed_cons = list( + coramin.relaxations.nonrelaxation_component_data_objects( + relaxed_m, pe.Constraint, active=True, sort=True, descend_into=True + ) + ) + relaxed_rels = list( + coramin.relaxations.relaxation_data_objects( + relaxed_m, descend_into=True, active=True, sort=True + ) + ) + decomposed_vars = list( + coramin.relaxations.nonrelaxation_component_data_objects( + decomposed_m, pe.Var, sort=True, descend_into=True + ) + ) + decomposed_cons = list( + coramin.relaxations.nonrelaxation_component_data_objects( + decomposed_m, pe.Constraint, active=True, sort=True, descend_into=True + ) + ) + decomposed_rels = list( + coramin.relaxations.relaxation_data_objects( + decomposed_m, descend_into=True, active=True, sort=True + ) + ) linking_cons = list() for stage in range(decomposed_m.num_stages()): for block in decomposed_m.stage_blocks(stage): @@ -515,13 +598,17 @@ def helper(self, case, min_partition_ratio, expected_termination): for c in linking_cons: for v in identify_variables(c.body, include_fixed=True): extra_vars.add(v) - for v in coramin.relaxations.nonrelaxation_component_data_objects(decomposed_m, pe.Var, descend_into=True): + for v in coramin.relaxations.nonrelaxation_component_data_objects( + decomposed_m, pe.Var, descend_into=True + ): if 'dbt_partition_vars' in str(v) or 'obj_var' in str(v): extra_vars.add(v) extra_vars = extra_vars - relaxed_vars_mapped partition_cons = ComponentSet() obj_cons = ComponentSet() - for c in coramin.relaxations.nonrelaxation_component_data_objects(decomposed_m, pe.Constraint, active=True, descend_into=True): + for c in coramin.relaxations.nonrelaxation_component_data_objects( + decomposed_m, pe.Constraint, active=True, descend_into=True + ): if 'dbt_partition_cons' in str(c): partition_cons.add(c) elif 'obj_con' in str(c): @@ -563,16 +650,32 @@ def _reformat(s: str) -> str: self.assertEqual(len(relaxed_rels), len(decomposed_rels)) def test_decompose1(self): - self.helper('pglib_opf_case5_pjm.m', min_partition_ratio=1.5, expected_termination=coramin.domain_reduction.dbt.DecompositionStatus.problem_too_small) + self.helper( + 'pglib_opf_case5_pjm.m', + min_partition_ratio=1.5, + expected_termination=coramin.domain_reduction.dbt.DecompositionStatus.problem_too_small, + ) def test_decompose2(self): - self.helper('pglib_opf_case30_ieee.m', min_partition_ratio=1.5, expected_termination=coramin.domain_reduction.dbt.DecompositionStatus.normal) + self.helper( + 'pglib_opf_case30_ieee.m', + min_partition_ratio=1.5, + expected_termination=coramin.domain_reduction.dbt.DecompositionStatus.normal, + ) def test_decompose3(self): - self.helper('pglib_opf_case118_ieee.m', min_partition_ratio=1.5, expected_termination=coramin.domain_reduction.dbt.DecompositionStatus.normal) + self.helper( + 'pglib_opf_case118_ieee.m', + min_partition_ratio=1.5, + expected_termination=coramin.domain_reduction.dbt.DecompositionStatus.normal, + ) def test_decompose4(self): - self.helper('pglib_opf_case14_ieee.m', min_partition_ratio=1.4, expected_termination=coramin.domain_reduction.dbt.DecompositionStatus.normal) + self.helper( + 'pglib_opf_case14_ieee.m', + min_partition_ratio=1.4, + expected_termination=coramin.domain_reduction.dbt.DecompositionStatus.normal, + ) class TestVarsToTightenByBlock(unittest.TestCase): @@ -598,21 +701,25 @@ def test_vars_to_tighten_by_block(self): b2.c = pe.Constraint(expr=b2.x + b2.y + b2.z == 0) b1.r = coramin.relaxations.PWUnivariateRelaxation() - b1.r.set_input(x=b1.x, - aux_var=b1.aux, - shape=coramin.utils.FunctionShape.CONVEX, - f_x_expr=pe.exp(b1.x)) + b1.r.set_input( + x=b1.x, + aux_var=b1.aux, + shape=coramin.utils.FunctionShape.CONVEX, + f_x_expr=pe.exp(b1.x), + ) b1.r.rebuild() b2.r = coramin.relaxations.PWXSquaredRelaxation() - b2.r.set_input(x=b2.x, - aux_var=b2.aux, - relaxation_side=coramin.utils.RelaxationSide.UNDER) + b2.r.set_input( + x=b2.x, aux_var=b2.aux, relaxation_side=coramin.utils.RelaxationSide.UNDER + ) b2.r.rebuild() m.linking_constraints.add(b1.z == b2.z) - vars_to_tighten_by_block = collect_vars_to_tighten_by_block(m, method='full_space') + vars_to_tighten_by_block = collect_vars_to_tighten_by_block( + m, method='full_space' + ) self.assertEqual(len(vars_to_tighten_by_block), 3) vars_to_tighten = vars_to_tighten_by_block[m] self.assertEqual(len(vars_to_tighten), 0) @@ -660,13 +767,23 @@ def get_model(self): b0.y = pe.Var(bounds=(-5, 5)) b0.p = pe.Param(initialize=1.0, mutable=True) b0.c = coramin.relaxations.PWUnivariateRelaxation() - b0.c.build(x=b0.x, aux_var=b0.y, shape=coramin.utils.FunctionShape.CONVEX, f_x_expr=b0.p*b0.x) + b0.c.build( + x=b0.x, + aux_var=b0.y, + shape=coramin.utils.FunctionShape.CONVEX, + f_x_expr=b0.p * b0.x, + ) b1.x = pe.Var(bounds=(-5, 5)) b1.y = pe.Var(bounds=(-5, 5)) b1.p = pe.Param(initialize=1.0, mutable=True) b1.c = coramin.relaxations.PWUnivariateRelaxation() - b1.c.build(x=b1.x, aux_var=b1.y, shape=coramin.utils.FunctionShape.CONVEX, f_x_expr=b1.p*b1.x) + b1.c.build( + x=b1.x, + aux_var=b1.y, + shape=coramin.utils.FunctionShape.CONVEX, + f_x_expr=b1.p * b1.x, + ) m.linking_constraints.add(b0.y == b1.y) @@ -677,7 +794,12 @@ def test_full_space(self): b0 = m.children[0] b1 = m.children[1] opt = appsi.solvers.Gurobi() - perform_dbt(relaxation=m, solver=opt, obbt_method=OBBTMethod.FULL_SPACE, filter_method=FilterMethod.NONE) + perform_dbt( + relaxation=m, + solver=opt, + obbt_method=OBBTMethod.FULL_SPACE, + filter_method=FilterMethod.NONE, + ) self.assertAlmostEqual(b0.x.lb, -1) self.assertAlmostEqual(b0.x.ub, 1) self.assertAlmostEqual(b0.y.lb, -5) @@ -692,7 +814,12 @@ def test_leaves(self): b0 = m.children[0] b1 = m.children[1] opt = appsi.solvers.Gurobi() - perform_dbt(relaxation=m, solver=opt, obbt_method=OBBTMethod.LEAVES, filter_method=FilterMethod.NONE) + perform_dbt( + relaxation=m, + solver=opt, + obbt_method=OBBTMethod.LEAVES, + filter_method=FilterMethod.NONE, + ) self.assertAlmostEqual(b0.x.lb, -1) self.assertAlmostEqual(b0.x.ub, 1) self.assertAlmostEqual(b0.y.lb, -5) @@ -707,7 +834,12 @@ def test_dbt(self): b0 = m.children[0] b1 = m.children[1] opt = appsi.solvers.Gurobi() - perform_dbt(relaxation=m, solver=opt, obbt_method=OBBTMethod.DECOMPOSED, filter_method=FilterMethod.NONE) + perform_dbt( + relaxation=m, + solver=opt, + obbt_method=OBBTMethod.DECOMPOSED, + filter_method=FilterMethod.NONE, + ) self.assertAlmostEqual(b0.x.lb, -1) self.assertAlmostEqual(b0.x.ub, 1) self.assertAlmostEqual(b0.y.lb, -1) @@ -722,7 +854,12 @@ def test_dbt_with_filter(self): b0 = m.children[0] b1 = m.children[1] opt = appsi.solvers.Gurobi() - perform_dbt(relaxation=m, solver=opt, obbt_method=OBBTMethod.DECOMPOSED, filter_method=FilterMethod.AGGRESSIVE) + perform_dbt( + relaxation=m, + solver=opt, + obbt_method=OBBTMethod.DECOMPOSED, + filter_method=FilterMethod.AGGRESSIVE, + ) self.assertAlmostEqual(b0.x.lb, -1) self.assertAlmostEqual(b0.x.ub, 1) self.assertAlmostEqual(b0.y.lb, -1) @@ -765,11 +902,11 @@ def create_model(self): b4.x11 = pe.Var(bounds=(0.5, 5)) b4.x12 = pe.Var(bounds=(0.5, 5)) - b1.c1 = pe.Constraint(expr=b1.x1 == b1.x2 ** 2 - b1.x3 ** 2) + b1.c1 = pe.Constraint(expr=b1.x1 == b1.x2**2 - b1.x3**2) b1.c2 = pe.Constraint(expr=b1.x2 == pe.log(b1.x3) + b1.x3) b2.c1 = pe.Constraint(expr=b2.x4 == b2.x5 * b2.x6) - b2.c2 = pe.Constraint(expr=b2.x5 == b2.x6 ** 2) + b2.c2 = pe.Constraint(expr=b2.x5 == b2.x6**2) b3.c1 = pe.Constraint(expr=b3.x7 == pe.log(b3.x8) - pe.log(b3.x9)) b3.c2 = pe.Constraint(expr=b3.x8 + b3.x9 == 4) @@ -782,7 +919,19 @@ def create_model(self): m.linking_constraints.add(b1.x3 == b3.x9) m.obj = pe.Objective( - expr=b1.x1 + b1.x2 + b1.x3 + b2.x4 + b2.x5 + b2.x6 + b3.x7 + b3.x8 + b3.x9 + b4.x10 + b4.x11 + b4.x12) + expr=b1.x1 + + b1.x2 + + b1.x3 + + b2.x4 + + b2.x5 + + b2.x6 + + b3.x7 + + b3.x8 + + b3.x9 + + b4.x10 + + b4.x11 + + b4.x12 + ) return m @@ -798,8 +947,12 @@ def test_bounds_tightening(self): opt = coramin.algorithms.ECPBounder(subproblem_solver=appsi.solvers.Gurobi()) opt.config.keep_cuts = False opt.config.feasibility_tol = 1e-5 - coramin.domain_reduction.perform_dbt(m, opt, filter_method=coramin.domain_reduction.FilterMethod.NONE, - parallel=True) + coramin.domain_reduction.perform_dbt( + m, + opt, + filter_method=coramin.domain_reduction.FilterMethod.NONE, + parallel=True, + ) m.write(f'rank{rank}.lp') comm.Barrier() if rank == 0: @@ -813,8 +966,13 @@ def test_bounds_tightening(self): opt = coramin.algorithms.ECPBounder(subproblem_solver=appsi.solvers.Gurobi()) opt.config.keep_cuts = False opt.config.feasibility_tol = 1e-5 - coramin.domain_reduction.perform_dbt(m, opt, filter_method=coramin.domain_reduction.FilterMethod.NONE, - parallel=True, update_relaxations_between_stages=False) + coramin.domain_reduction.perform_dbt( + m, + opt, + filter_method=coramin.domain_reduction.FilterMethod.NONE, + parallel=True, + update_relaxations_between_stages=False, + ) m.write(f'rank{rank}.lp') comm.Barrier() if rank == 0: diff --git a/pyomo/contrib/coramin/domain_reduction/tests/test_filters.py b/pyomo/contrib/coramin/domain_reduction/tests/test_filters.py index 8e60e90f591..56ea31686f3 100644 --- a/pyomo/contrib/coramin/domain_reduction/tests/test_filters.py +++ b/pyomo/contrib/coramin/domain_reduction/tests/test_filters.py @@ -14,7 +14,10 @@ def test_basic_filter(self): coramin.relaxations.relax(m, in_place=True) opt = appsi.solvers.Gurobi() res = opt.solve(m) - vars_to_min, vars_to_max = coramin.domain_reduction.filter_variables_from_solution([m.x]) + ( + vars_to_min, + vars_to_max, + ) = coramin.domain_reduction.filter_variables_from_solution([m.x]) self.assertIn(m.x, vars_to_max) self.assertNotIn(m.x, vars_to_min) @@ -26,7 +29,8 @@ def test_aggressive_filter(self): m.c = pe.Constraint(expr=m.y == -m.x**2) coramin.relaxations.relax(m, in_place=True) opt = appsi.solvers.Gurobi() - vars_to_min, vars_to_max = coramin.domain_reduction.aggressive_filter(candidate_variables=[m.x], relaxation=m, - solver=opt) + vars_to_min, vars_to_max = coramin.domain_reduction.aggressive_filter( + candidate_variables=[m.x], relaxation=m, solver=opt + ) self.assertNotIn(m.x, vars_to_max) self.assertNotIn(m.x, vars_to_min) diff --git a/pyomo/contrib/coramin/domain_reduction/tests/test_obbt.py b/pyomo/contrib/coramin/domain_reduction/tests/test_obbt.py index c2535c5254a..23a6c2bf783 100644 --- a/pyomo/contrib/coramin/domain_reduction/tests/test_obbt.py +++ b/pyomo/contrib/coramin/domain_reduction/tests/test_obbt.py @@ -24,13 +24,14 @@ def test_quad(self): x_points = [-5.0, 5.0] model.under_estimators = pyo.ConstraintList() for xp in x_points: - m = 2*xp + m = 2 * xp b = -(xp**2) - model.under_estimators.add(model.y >= m*model.x + b) + model.under_estimators.add(model.y >= m * model.x + b) solver = appsi.solvers.Ipopt() - (lower, upper) = coramin.domain_reduction.perform_obbt(model=model, solver=solver, varlist=[model.x, model.y], - update_bounds=True) + (lower, upper) = coramin.domain_reduction.perform_obbt( + model=model, solver=solver, varlist=[model.x, model.y], update_bounds=True + ) self.assertAlmostEqual(pyo.value(model.x.lb), -5.0, delta=1e-6) self.assertAlmostEqual(pyo.value(model.x.ub), 5.0, delta=1e-6) self.assertAlmostEqual(pyo.value(model.y.lb), -25.0, delta=1e-6) @@ -52,11 +53,13 @@ def test_passing_component_not_list(self): model.under_estimators = pyo.ConstraintList() for xp in x_points: m = 2 * xp - b = -(xp ** 2) + b = -(xp**2) model.under_estimators.add(model.y >= m * model.x + b) solver = appsi.solvers.Ipopt() - (lower, upper) = coramin.domain_reduction.perform_obbt(model=model, solver=solver, varlist=model.y, update_bounds=True) + (lower, upper) = coramin.domain_reduction.perform_obbt( + model=model, solver=solver, varlist=model.y, update_bounds=True + ) self.assertAlmostEqual(pyo.value(model.x.lb), -5.0, delta=1e-6) self.assertAlmostEqual(pyo.value(model.x.ub), 5.0, delta=1e-6) self.assertAlmostEqual(pyo.value(model.y.lb), -25.0, delta=1e-6) @@ -77,13 +80,15 @@ def test_passing_indexed_component_not_list(self): model.under_estimators = pyo.ConstraintList() for xp in x_points: m = 2 * xp - b = -(xp ** 2) + b = -(xp**2) model.under_estimators.add(model.y['A'] >= m * model.x + b) model.con = pyo.Constraint(expr=model.y['A'] == 1 + model.y['B']) solver = appsi.solvers.Ipopt() - lower, upper = coramin.domain_reduction.perform_obbt(model=model, solver=solver, varlist=model.y, update_bounds=True) + lower, upper = coramin.domain_reduction.perform_obbt( + model=model, solver=solver, varlist=model.y, update_bounds=True + ) self.assertAlmostEqual(pyo.value(model.x.lb), -5.0, delta=1e-6) self.assertAlmostEqual(pyo.value(model.x.ub), 5.0, delta=1e-6) self.assertAlmostEqual(pyo.value(model.y['A'].lb), -25.0, delta=1e-6) @@ -94,7 +99,7 @@ def test_passing_indexed_component_not_list(self): self.assertAlmostEqual(upper[0], 100.0, delta=1e-6) self.assertAlmostEqual(lower[1], -26.0, delta=1e-6) self.assertAlmostEqual(upper[1], 99.0, delta=1e-6) - + def test_too_many_obj(self): model = pyo.ConcreteModel() model.x = pyo.Var(bounds=(-5.0, 5.0)) @@ -105,8 +110,13 @@ def test_too_many_obj(self): solver = pyo.SolverFactory('ipopt') with self.assertRaises(ValueError): - coramin.domain_reduction.perform_obbt(model=model, solver=solver, varlist=[model.x, model.y], - objective_bound=0.0, update_bounds=True) + coramin.domain_reduction.perform_obbt( + model=model, + solver=solver, + varlist=[model.x, model.y], + objective_bound=0.0, + update_bounds=True, + ) if __name__ == '__main__': diff --git a/pyomo/contrib/coramin/examples/alpha_bb.py b/pyomo/contrib/coramin/examples/alpha_bb.py index c3d53eb6de4..b14390d9eac 100644 --- a/pyomo/contrib/coramin/examples/alpha_bb.py +++ b/pyomo/contrib/coramin/examples/alpha_bb.py @@ -13,7 +13,7 @@ def main(): m.c.build( aux_var=m.z, - f_x_expr=m.x*pe.log(m.x/m.y), + f_x_expr=m.x * pe.log(m.x / m.y), relaxation_side=coramin.RelaxationSide.UNDER, eigenvalue_bounder=coramin.EigenValueBounder.GershgorinWithSimplification, ) @@ -37,8 +37,7 @@ def main(): mip_opt = appsi.solvers.Gurobi() nlp_opt = appsi.solvers.Ipopt() eigenvalue_opt = coramin.algorithms.MultiTree( - mip_solver=mip_opt, - nlp_solver=nlp_opt, + mip_solver=mip_opt, nlp_solver=nlp_opt ) eigenvalue_opt.config.convexity_effort = 'medium' m.c.hessian.opt = eigenvalue_opt diff --git a/pyomo/contrib/coramin/examples/dbt.py b/pyomo/contrib/coramin/examples/dbt.py index 90d175f51ac..3284ed534ef 100644 --- a/pyomo/contrib/coramin/examples/dbt.py +++ b/pyomo/contrib/coramin/examples/dbt.py @@ -25,14 +25,22 @@ # perform decomposition print('Decomposing relaxation') -relaxation, component_map, termination_reason = coramin.domain_reduction.decompose_model(relaxation, max_leaf_nnz=1000) +( + relaxation, + component_map, + termination_reason, +) = coramin.domain_reduction.decompose_model(relaxation, max_leaf_nnz=1000) # Add more outer approximation points for the second order cone constraints print('Adding extra outer-approximation points for SOC constraints') -for b in coramin.relaxations.relaxation_data_objects(relaxation, descend_into=True, active=True, sort=True): +for b in coramin.relaxations.relaxation_data_objects( + relaxation, descend_into=True, active=True, sort=True +): if isinstance(b, coramin.relaxations.MultivariateRelaxationData): b.clear_oa_points() - for bnd_combination in itertools.product(*[itertools.product(['L', 'U'], [v]) for v in b.get_rhs_vars()]): + for bnd_combination in itertools.product( + *[itertools.product(['L', 'U'], [v]) for v in b.get_rhs_vars()] + ): bnd_dict = pe.ComponentMap() for lower_or_upper, v in bnd_combination: if lower_or_upper == 'L': @@ -49,7 +57,9 @@ b.add_oa_point(var_values=bnd_dict) # rebuild the relaxations -for b in coramin.relaxations.relaxation_data_objects(relaxation, descend_into=True, active=True, sort=True): +for b in coramin.relaxations.relaxation_data_objects( + relaxation, descend_into=True, active=True, sort=True +): b.rebuild() # create solvers @@ -69,22 +79,38 @@ assert res.solver.termination_condition == pe.TerminationCondition.optimal lb = pe.value(coramin.utils.get_objective(relaxation)) gap = (ub - lb) / ub * 100 -print('{ub:<20}{lb:<20}{gap:<20}{time:<20}'.format(ub='UB', lb='LB', gap='% gap', time='Time')) +print( + '{ub:<20}{lb:<20}{gap:<20}{time:<20}'.format( + ub='UB', lb='LB', gap='% gap', time='Time' + ) +) t0 = time.time() -print('{ub:<20.2f}{lb:<20.2f}{gap:<20.2f}{time:<20.2f}'.format(ub=ub, lb=lb, gap=gap, time=time.time() - t0)) +print( + '{ub:<20.2f}{lb:<20.2f}{gap:<20.2f}{time:<20.2f}'.format( + ub=ub, lb=lb, gap=gap, time=time.time() - t0 + ) +) for _iter in range(3): - coramin.domain_reduction.perform_dbt(relaxation=relaxation, - solver=rel_opt, - obbt_method=coramin.domain_reduction.OBBTMethod.DECOMPOSED, - filter_method=coramin.domain_reduction.FilterMethod.AGGRESSIVE, - objective_bound=ub, - with_progress_bar=True) - for r in coramin.relaxations.relaxation_data_objects(relaxation, descend_into=True, active=True, sort=True): + coramin.domain_reduction.perform_dbt( + relaxation=relaxation, + solver=rel_opt, + obbt_method=coramin.domain_reduction.OBBTMethod.DECOMPOSED, + filter_method=coramin.domain_reduction.FilterMethod.AGGRESSIVE, + objective_bound=ub, + with_progress_bar=True, + ) + for r in coramin.relaxations.relaxation_data_objects( + relaxation, descend_into=True, active=True, sort=True + ): r.rebuild() rel_opt.set_instance(relaxation) res = rel_opt.solve(save_results=False) assert res.solver.termination_condition == pe.TerminationCondition.optimal lb = pe.value(coramin.utils.get_objective(relaxation)) gap = (ub - lb) / ub * 100 - print('{ub:<20.2f}{lb:<20.2f}{gap:<20.2f}{time:<20.2f}'.format(ub=ub, lb=lb, gap=gap, time=time.time() - t0)) + print( + '{ub:<20.2f}{lb:<20.2f}{gap:<20.2f}{time:<20.2f}'.format( + ub=ub, lb=lb, gap=gap, time=time.time() - t0 + ) + ) diff --git a/pyomo/contrib/coramin/examples/dbt2.py b/pyomo/contrib/coramin/examples/dbt2.py index 1b2fd08214f..a2520a96d36 100644 --- a/pyomo/contrib/coramin/examples/dbt2.py +++ b/pyomo/contrib/coramin/examples/dbt2.py @@ -19,16 +19,28 @@ get_minlplib(problem_name='camshape800') print('Creating NLP and relaxation') -nlp = read_osil('minlplib/osil/camshape800.osil', objective_prefix='obj_', constraint_prefix='con_') -relaxation = coramin.relaxations.relax(nlp, in_place=False, use_fbbt=True, fbbt_options={'deactivate_satisfied_constraints': True, - 'max_iter': 2}) +nlp = read_osil( + 'minlplib/osil/camshape800.osil', objective_prefix='obj_', constraint_prefix='con_' +) +relaxation = coramin.relaxations.relax( + nlp, + in_place=False, + use_fbbt=True, + fbbt_options={'deactivate_satisfied_constraints': True, 'max_iter': 2}, +) # perform decomposition print('Decomposing relaxation') -relaxation, component_map, termination_reason = coramin.domain_reduction.decompose_model(relaxation, max_leaf_nnz=1000) +( + relaxation, + component_map, + termination_reason, +) = coramin.domain_reduction.decompose_model(relaxation, max_leaf_nnz=1000) # rebuild the relaxations -for b in coramin.relaxations.relaxation_data_objects(relaxation, descend_into=True, active=True, sort=True): +for b in coramin.relaxations.relaxation_data_objects( + relaxation, descend_into=True, active=True, sort=True +): b.rebuild() # create solvers @@ -49,23 +61,37 @@ lb = pe.value(coramin.utils.get_objective(relaxation)) gap = (ub - lb) / abs(ub) * 100 var_bounds = pe.ComponentMap() -for r in coramin.relaxations.relaxation_data_objects(relaxation, descend_into=True, active=True, sort=True): +for r in coramin.relaxations.relaxation_data_objects( + relaxation, descend_into=True, active=True, sort=True +): for v in r.get_rhs_vars(): var_bounds[v] = v.ub - v.lb avg_bound_range = sum(var_bounds.values()) / len(var_bounds) -print('{ub:<20}{lb:<20}{gap:<20}{avg_rng:<20}{time:<20}'.format(ub='UB', lb='LB', gap='% gap', avg_rng='Avg Var Range', time='Time')) +print( + '{ub:<20}{lb:<20}{gap:<20}{avg_rng:<20}{time:<20}'.format( + ub='UB', lb='LB', gap='% gap', avg_rng='Avg Var Range', time='Time' + ) +) t0 = time.time() -print('{ub:<20.3f}{lb:<20.3f}{gap:<20.3f}{avg_rng:<20.3e}{time:<20.3f}'.format(ub=ub, lb=lb, gap=gap, avg_rng=avg_bound_range, time=time.time() - t0)) +print( + '{ub:<20.3f}{lb:<20.3f}{gap:<20.3f}{avg_rng:<20.3e}{time:<20.3f}'.format( + ub=ub, lb=lb, gap=gap, avg_rng=avg_bound_range, time=time.time() - t0 + ) +) # Perform bounds tightening for _iter in range(3): - coramin.domain_reduction.perform_dbt(relaxation=relaxation, - solver=rel_opt, - obbt_method=coramin.domain_reduction.OBBTMethod.DECOMPOSED, - filter_method=coramin.domain_reduction.FilterMethod.AGGRESSIVE, - objective_bound=ub, - with_progress_bar=True) - for r in coramin.relaxations.relaxation_data_objects(relaxation, descend_into=True, active=True, sort=True): + coramin.domain_reduction.perform_dbt( + relaxation=relaxation, + solver=rel_opt, + obbt_method=coramin.domain_reduction.OBBTMethod.DECOMPOSED, + filter_method=coramin.domain_reduction.FilterMethod.AGGRESSIVE, + objective_bound=ub, + with_progress_bar=True, + ) + for r in coramin.relaxations.relaxation_data_objects( + relaxation, descend_into=True, active=True, sort=True + ): r.rebuild() rel_opt.set_instance(relaxation) res = rel_opt.solve(save_results=False) @@ -73,8 +99,14 @@ lb = pe.value(coramin.utils.get_objective(relaxation)) gap = (ub - lb) / abs(ub) * 100 var_bounds = pe.ComponentMap() - for r in coramin.relaxations.relaxation_data_objects(relaxation, descend_into=True, active=True, sort=True): + for r in coramin.relaxations.relaxation_data_objects( + relaxation, descend_into=True, active=True, sort=True + ): for v in r.get_rhs_vars(): var_bounds[v] = v.ub - v.lb avg_bound_range = sum(var_bounds.values()) / len(var_bounds) - print('{ub:<20.3f}{lb:<20.3f}{gap:<20.3f}{avg_rng:<20.3e}{time:<20.3f}'.format(ub=ub, lb=lb, gap=gap, avg_rng=avg_bound_range, time=time.time() - t0)) + print( + '{ub:<20.3f}{lb:<20.3f}{gap:<20.3f}{avg_rng:<20.3e}{time:<20.3f}'.format( + ub=ub, lb=lb, gap=gap, avg_rng=avg_bound_range, time=time.time() - t0 + ) + ) diff --git a/pyomo/contrib/coramin/examples/ex.py b/pyomo/contrib/coramin/examples/ex.py index 5f5aceff845..6fb8252dc5c 100644 --- a/pyomo/contrib/coramin/examples/ex.py +++ b/pyomo/contrib/coramin/examples/ex.py @@ -17,7 +17,7 @@ # Build and solve the NLP nlp = pe.ConcreteModel() nlp.x = pe.Var(bounds=(-2, 2)) -nlp.obj = pe.Objective(expr=nlp.x**4 - 3*nlp.x**2 + nlp.x) +nlp.obj = pe.Objective(expr=nlp.x**4 - 3 * nlp.x**2 + nlp.x) opt = pe.SolverFactory('ipopt') res = opt.solve(nlp) ub = pe.value(nlp.obj) @@ -41,7 +41,7 @@ rel.x2_con.build(x=rel.x, aux_var=rel.x2, use_linear_relaxation=True) rel.x4_con = coramin.relaxations.PWXSquaredRelaxation() rel.x4_con.build(x=rel.x2, aux_var=rel.x4, use_linear_relaxation=True) -rel.obj = pe.Objective(expr=rel.x4 - 3*rel.x2 + rel.x) +rel.obj = pe.Objective(expr=rel.x4 - 3 * rel.x2 + rel.x) # Now solve the relaxation and refine the convex sides of the constraints with add_cut @@ -54,7 +54,9 @@ print('gap: ' + str(100 * abs(ub - lb) / abs(ub)) + ' %') for _iter in range(10): - for b in rel.component_data_objects(pe.Block, active=True, sort=True, descend_into=True): + for b in rel.component_data_objects( + pe.Block, active=True, sort=True, descend_into=True + ): if isinstance(b, coramin.relaxations.BaseRelaxationData): b.add_cut() res = opt.solve(rel) @@ -62,7 +64,9 @@ print('gap: ' + str(100 * abs(ub - lb) / abs(ub)) + ' %') # we want to discard the cuts generated above just to demonstrate OBBT -for b in rel.component_data_objects(pe.Block, active=True, sort=True, descend_into=True): +for b in rel.component_data_objects( + pe.Block, active=True, sort=True, descend_into=True +): if isinstance(b, coramin.relaxations.BasePWRelaxationData): b.clear_oa_points() b.rebuild() @@ -76,11 +80,11 @@ print('gap: ' + str(100 * abs(ub - lb) / abs(ub)) + ' %') for _iter in range(10): coramin.domain_reduction.perform_obbt(rel, opt, [rel.x, rel.x2], objective_bound=ub) - for b in rel.component_data_objects(pe.Block, active=True, sort=True, descend_into=True): + for b in rel.component_data_objects( + pe.Block, active=True, sort=True, descend_into=True + ): if isinstance(b, coramin.relaxations.BasePWRelaxationData): b.rebuild() res = opt.solve(rel) lb = pe.value(rel.obj) print('gap: ' + str(100 * abs(ub - lb) / abs(ub)) + ' %') - - diff --git a/pyomo/contrib/coramin/examples/rosenbrock.py b/pyomo/contrib/coramin/examples/rosenbrock.py index acb1ac4cc52..cf55ce967d6 100644 --- a/pyomo/contrib/coramin/examples/rosenbrock.py +++ b/pyomo/contrib/coramin/examples/rosenbrock.py @@ -8,7 +8,7 @@ def create_nlp(a, b): m.x = pe.Var(bounds=(-20.0, 20.0)) m.y = pe.Var(bounds=(-20.0, 20.0)) - m.objective = pe.Objective(expr=(a - m.x)**2 + b*(m.y - m.x**2)**2) + m.objective = pe.Objective(expr=(a - m.x) ** 2 + b * (m.y - m.x**2) ** 2) return m @@ -20,7 +20,7 @@ def create_relaxation(a, b): m.y = pe.Var(bounds=(-20.0, 20.0)) m.z = pe.Var() - m.objective = pe.Objective(expr=(a - m.x)**2 + b*m.z**2) + m.objective = pe.Objective(expr=(a - m.x) ** 2 + b * m.z**2) m.con1 = pe.Constraint(expr=m.z == m.y - m.x_sq) m.x_sq_con = coramin.relaxations.PWXSquaredRelaxation() m.x_sq_con.build(x=m.x, aux_var=m.x_sq, use_linear_relaxation=True) diff --git a/pyomo/contrib/coramin/relaxations/__init__.py b/pyomo/contrib/coramin/relaxations/__init__.py index 5f8f47e4463..a4088614410 100644 --- a/pyomo/contrib/coramin/relaxations/__init__.py +++ b/pyomo/contrib/coramin/relaxations/__init__.py @@ -1,4 +1,9 @@ -from .relaxations_base import BaseRelaxation, BaseRelaxationData, BasePWRelaxation, BasePWRelaxationData +from .relaxations_base import ( + BaseRelaxation, + BaseRelaxationData, + BasePWRelaxation, + BasePWRelaxationData, +) from .mccormick import PWMcCormickRelaxation, PWMcCormickRelaxationData from .segments import compute_k_segment_points from .univariate import PWXSquaredRelaxation, PWXSquaredRelaxationData diff --git a/pyomo/contrib/coramin/relaxations/_utils.py b/pyomo/contrib/coramin/relaxations/_utils.py index b8e79e14705..b1a4ba3b582 100644 --- a/pyomo/contrib/coramin/relaxations/_utils.py +++ b/pyomo/contrib/coramin/relaxations/_utils.py @@ -52,18 +52,31 @@ def check_var_pts(x, x_pts=None): msg = None if xub < xlb: - msg = 'Lower bound is larger than upper bound:\n' + var_info_str(x) + bnds_info_str(xlb, xub) + msg = ( + 'Lower bound is larger than upper bound:\n' + + var_info_str(x) + + bnds_info_str(xlb, xub) + ) raise_error = True if x_pts is not None: - ordered = all(x_pts[i] <= x_pts[i+1] for i in range(len(x_pts)-1)) + ordered = all(x_pts[i] <= x_pts[i + 1] for i in range(len(x_pts) - 1)) if not ordered: - msg = 'x_pts must be ordered:\n' + var_info_str(x) + bnds_info_str(xlb, xub) + x_pts_info_str(x_pts) + msg = ( + 'x_pts must be ordered:\n' + + var_info_str(x) + + bnds_info_str(xlb, xub) + + x_pts_info_str(x_pts) + ) raise_error = True if xlb != x_pts[0] or xub != x_pts[-1]: - msg = ('end points of the x_pts list must be equal to the bounds on the x variable:\n' + var_info_str(x) + - bnds_info_str(xlb, xub) + x_pts_info_str(x_pts)) + msg = ( + 'end points of the x_pts list must be equal to the bounds on the x variable:\n' + + var_info_str(x) + + bnds_info_str(xlb, xub) + + x_pts_info_str(x_pts) + ) raise_error = True if raise_error: diff --git a/pyomo/contrib/coramin/relaxations/alphabb.py b/pyomo/contrib/coramin/relaxations/alphabb.py index 2b88da6a05b..8065e082bea 100644 --- a/pyomo/contrib/coramin/relaxations/alphabb.py +++ b/pyomo/contrib/coramin/relaxations/alphabb.py @@ -1,6 +1,9 @@ from pyomo.contrib.coramin.utils.coramin_enums import EigenValueBounder, RelaxationSide from pyomo.contrib.coramin.relaxations.custom_block import declare_custom_block -from pyomo.contrib.coramin.relaxations.relaxations_base import BaseRelaxationData, ComponentWeakRef +from pyomo.contrib.coramin.relaxations.relaxations_base import ( + BaseRelaxationData, + ComponentWeakRef, +) from pyomo.contrib.coramin.relaxations.hessian import Hessian from typing import Optional, Tuple from pyomo.core.base.var import _GeneralVarData @@ -83,7 +86,7 @@ def set_input( use_linear_relaxation=use_linear_relaxation, large_coef=large_coef, small_coef=small_coef, - safety_tol=safety_tol + safety_tol=safety_tol, ) self._xs = tuple(identify_variables(f_x_expr, include_fixed=False)) self._aux_var_ref.set_component(aux_var) @@ -134,15 +137,19 @@ def relaxation_side(self): @relaxation_side.setter def relaxation_side(self, val): if val != self.relaxation_side: - raise ValueError('Cannot change the relaxation side of an AlphaBBRelaxation') + raise ValueError( + 'Cannot change the relaxation side of an AlphaBBRelaxation' + ) if val == RelaxationSide.BOTH: - raise ValueError('AlphaBBRelaxation only supports relaxation sides of UNDER or OVER, not BOTH.') + raise ValueError( + 'AlphaBBRelaxation only supports relaxation sides of UNDER or OVER, not BOTH.' + ) def rebuild(self, build_nonlinear_constraint=False, ensure_oa_at_vertices=True): if self.relaxation_side == RelaxationSide.UNDER: alpha = max(0, -0.5 * self._hessian.get_minimum_eigenvalue()) else: - alpha = max(0, 0.5*self._hessian.get_maximum_eigenvalue()) + alpha = max(0, 0.5 * self._hessian.get_maximum_eigenvalue()) alpha = -alpha if self._alpha is None: del self._alpha, self._alphabb_rhs, self._var_set @@ -162,5 +169,7 @@ def rebuild(self, build_nonlinear_constraint=False, ensure_oa_at_vertices=True): self._lb_params[ndx].value = v.lb self._ub_params[ndx].value = v.ub - super().rebuild(build_nonlinear_constraint=build_nonlinear_constraint, - ensure_oa_at_vertices=ensure_oa_at_vertices) + super().rebuild( + build_nonlinear_constraint=build_nonlinear_constraint, + ensure_oa_at_vertices=ensure_oa_at_vertices, + ) diff --git a/pyomo/contrib/coramin/relaxations/auto_relax.py b/pyomo/contrib/coramin/relaxations/auto_relax.py index 751d048a8ff..a60d61d1b16 100644 --- a/pyomo/contrib/coramin/relaxations/auto_relax.py +++ b/pyomo/contrib/coramin/relaxations/auto_relax.py @@ -3,8 +3,13 @@ import pyomo.core.expr.numeric_expr as numeric_expr from pyomo.core.expr.visitor import ExpressionValueVisitor from pyomo.core.expr.numvalue import ( - nonpyomo_leaf_types, value, NumericValue, is_fixed, polynomial_degree, is_constant, - native_numeric_types + nonpyomo_leaf_types, + value, + NumericValue, + is_fixed, + polynomial_degree, + is_constant, + native_numeric_types, ) from pyomo.core.expr.numeric_expr import ExpressionBase from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr, fbbt @@ -12,14 +17,27 @@ from pyomo.core.base.constraint import Constraint import logging from .relaxations_base import BaseRelaxationData -from .univariate import PWUnivariateRelaxation, PWXSquaredRelaxation, PWCosRelaxation, PWSinRelaxation, PWArctanRelaxation +from .univariate import ( + PWUnivariateRelaxation, + PWXSquaredRelaxation, + PWCosRelaxation, + PWSinRelaxation, + PWArctanRelaxation, +) from .mccormick import PWMcCormickRelaxation from .multivariate import MultivariateRelaxation from .alphabb import AlphaBBRelaxation -from pyomo.contrib.coramin.utils.coramin_enums import RelaxationSide, FunctionShape, Effort, EigenValueBounder +from pyomo.contrib.coramin.utils.coramin_enums import ( + RelaxationSide, + FunctionShape, + Effort, + EigenValueBounder, +) from pyomo.gdp import Disjunct from pyomo.core.base.expression import _GeneralExpressionData, SimpleExpression -from pyomo.contrib.coramin.relaxations.iterators import nonrelaxation_component_data_objects +from pyomo.contrib.coramin.relaxations.iterators import ( + nonrelaxation_component_data_objects, +) from pyomo.contrib import appsi from pyomo.repn.standard_repn import generate_standard_repn from pyomo.contrib.fbbt import interval @@ -45,8 +63,7 @@ def __init__(self, *args): elif isinstance(i, NumericValue): entries.append(id(i)) else: - raise NotImplementedError( - f'unexpected entry: {str(i)}') + raise NotImplementedError(f'unexpected entry: {str(i)}') self.entries = entries self.hashable_entries = tuple(entries) @@ -110,7 +127,9 @@ def _get_aux_var(parent_block, expr): return _aux_var -def _relax_leaf_to_root_ProductExpression(node, values, aux_var_map, degree_map, parent_block, relaxation_side_map, counter): +def _relax_leaf_to_root_ProductExpression( + node, values, aux_var_map, degree_map, parent_block, relaxation_side_map, counter +): arg1, arg2 = values # The purpose of the next bit of code is to find common quadratic terms. For example, suppose we are relaxing @@ -154,7 +173,10 @@ def _relax_leaf_to_root_ProductExpression(node, values, aux_var_map, degree_map, elif degree_2 == 0: res = arg2 * arg1 degree_map[res] = degree_1 - elif arg1.__class__ == numeric_expr.MonomialTermExpression or arg2.__class__ == numeric_expr.MonomialTermExpression: + elif ( + arg1.__class__ == numeric_expr.MonomialTermExpression + or arg2.__class__ == numeric_expr.MonomialTermExpression + ): if arg1.__class__ == numeric_expr.MonomialTermExpression: coef1, arg1 = arg1.args else: @@ -167,16 +189,28 @@ def _relax_leaf_to_root_ProductExpression(node, values, aux_var_map, degree_map, _new_relaxation_side_map = ComponentMap() _reformulated = coef * (arg1 * arg2) _new_relaxation_side_map[_reformulated] = relaxation_side_map[node] - res = _relax_expr(expr=_reformulated, aux_var_map=aux_var_map, parent_block=parent_block, - relaxation_side_map=_new_relaxation_side_map, counter=counter, degree_map=degree_map) + res = _relax_expr( + expr=_reformulated, + aux_var_map=aux_var_map, + parent_block=parent_block, + relaxation_side_map=_new_relaxation_side_map, + counter=counter, + degree_map=degree_map, + ) degree_map[res] = 1 elif arg1 is arg2: # reformulate arg1 * arg2 as arg1**2 _new_relaxation_side_map = ComponentMap() _reformulated = arg1**2 _new_relaxation_side_map[_reformulated] = relaxation_side_map[node] - res = _relax_expr(expr=_reformulated, aux_var_map=aux_var_map, parent_block=parent_block, - relaxation_side_map=_new_relaxation_side_map, counter=counter, degree_map=degree_map) + res = _relax_expr( + expr=_reformulated, + aux_var_map=aux_var_map, + parent_block=parent_block, + relaxation_side_map=_new_relaxation_side_map, + counter=counter, + degree_map=degree_map, + ) degree_map[res] = 1 else: _aux_var = _get_aux_var(parent_block, arg1 * arg2) @@ -184,16 +218,20 @@ def _relax_leaf_to_root_ProductExpression(node, values, aux_var_map, degree_map, arg2 = replace_sub_expression_with_aux_var(arg2, parent_block) relaxation_side = relaxation_side_map[node] relaxation = PWMcCormickRelaxation() - relaxation.set_input(x1=arg1, x2=arg2, aux_var=_aux_var, relaxation_side=relaxation_side) + relaxation.set_input( + x1=arg1, x2=arg2, aux_var=_aux_var, relaxation_side=relaxation_side + ) aux_var_map[h1] = (_aux_var, relaxation) - setattr(parent_block.relaxations, 'rel'+str(counter), relaxation) + setattr(parent_block.relaxations, 'rel' + str(counter), relaxation) counter.increment() res = _aux_var degree_map[res] = 1 return res -def _relax_leaf_to_root_DivisionExpression(node, values, aux_var_map, degree_map, parent_block, relaxation_side_map, counter): +def _relax_leaf_to_root_DivisionExpression( + node, values, aux_var_map, degree_map, parent_block, relaxation_side_map, counter +): arg1, arg2 = values h1 = Hashable(arg1, arg2, 'div') if arg1.__class__ == numeric_expr.MonomialTermExpression: @@ -204,7 +242,7 @@ def _relax_leaf_to_root_DivisionExpression(node, values, aux_var_map, degree_map coef2, arg2 = arg2.args else: coef2 = 1 - coef = coef1/coef2 + coef = coef1 / coef2 degree_1 = degree_map[arg1] degree_2 = degree_map[arg2] @@ -233,25 +271,37 @@ def _relax_leaf_to_root_DivisionExpression(node, values, aux_var_map, degree_map degree_map[res] = 1 return res else: - _aux_var = _get_aux_var(parent_block, 1/arg2) + _aux_var = _get_aux_var(parent_block, 1 / arg2) arg2 = replace_sub_expression_with_aux_var(arg2, parent_block) relaxation_side = relaxation_side_map[node] degree_map[_aux_var] = 1 if compute_float_bounds_on_expr(arg2)[0] > 0: relaxation = PWUnivariateRelaxation() - relaxation.set_input(x=arg2, aux_var=_aux_var, relaxation_side=relaxation_side, f_x_expr=1/arg2, - shape=FunctionShape.CONVEX) + relaxation.set_input( + x=arg2, + aux_var=_aux_var, + relaxation_side=relaxation_side, + f_x_expr=1 / arg2, + shape=FunctionShape.CONVEX, + ) elif compute_float_bounds_on_expr(arg2)[1] < 0: relaxation = PWUnivariateRelaxation() - relaxation.set_input(x=arg2, aux_var=_aux_var, relaxation_side=relaxation_side, f_x_expr=1/arg2, - shape=FunctionShape.CONCAVE) + relaxation.set_input( + x=arg2, + aux_var=_aux_var, + relaxation_side=relaxation_side, + f_x_expr=1 / arg2, + shape=FunctionShape.CONCAVE, + ) else: _one = parent_block.aux_vars.add() _one.fix(1) relaxation = PWMcCormickRelaxation() - relaxation.set_input(x1=arg2, x2=_aux_var, aux_var=_one, relaxation_side=relaxation_side) + relaxation.set_input( + x1=arg2, x2=_aux_var, aux_var=_one, relaxation_side=relaxation_side + ) aux_var_map[h2] = (_aux_var, relaxation) - setattr(parent_block.relaxations, 'rel'+str(counter), relaxation) + setattr(parent_block.relaxations, 'rel' + str(counter), relaxation) counter.increment() res = coef * arg1 * _aux_var degree_map[res] = 1 @@ -274,9 +324,11 @@ def _relax_leaf_to_root_DivisionExpression(node, values, aux_var_map, degree_map else: relaxation_side = RelaxationSide.BOTH relaxation = PWMcCormickRelaxation() - relaxation.set_input(x1=arg2, x2=_aux_var, aux_var=arg1, relaxation_side=relaxation_side) + relaxation.set_input( + x1=arg2, x2=_aux_var, aux_var=arg1, relaxation_side=relaxation_side + ) aux_var_map[h1] = (_aux_var, relaxation) - setattr(parent_block.relaxations, 'rel'+str(counter), relaxation) + setattr(parent_block.relaxations, 'rel' + str(counter), relaxation) counter.increment() res = coef * _aux_var degree_map[_aux_var] = 1 @@ -284,7 +336,9 @@ def _relax_leaf_to_root_DivisionExpression(node, values, aux_var_map, degree_map return res -def _relax_quadratic(arg1, aux_var_map, relaxation_side, degree_map, parent_block, counter): +def _relax_quadratic( + arg1, aux_var_map, relaxation_side, degree_map, parent_block, counter +): _aux_var = _get_aux_var(parent_block, arg1**2) arg1 = replace_sub_expression_with_aux_var(arg1, parent_block) degree_map[_aux_var] = 1 @@ -296,7 +350,16 @@ def _relax_quadratic(arg1, aux_var_map, relaxation_side, degree_map, parent_bloc return _aux_var -def _relax_convex_pow(arg1, arg2, aux_var_map, relaxation_side, degree_map, parent_block, counter, swap=False): +def _relax_convex_pow( + arg1, + arg2, + aux_var_map, + relaxation_side, + degree_map, + parent_block, + counter, + swap=False, +): _aux_var = _get_aux_var(parent_block, arg1**arg2) if swap: arg2 = replace_sub_expression_with_aux_var(arg2, parent_block) @@ -306,28 +369,42 @@ def _relax_convex_pow(arg1, arg2, aux_var_map, relaxation_side, degree_map, pare _x = arg1 degree_map[_aux_var] = 1 relaxation = PWUnivariateRelaxation() - relaxation.set_input(x=_x, aux_var=_aux_var, relaxation_side=relaxation_side, f_x_expr=arg1 ** arg2, - shape=FunctionShape.CONVEX) + relaxation.set_input( + x=_x, + aux_var=_aux_var, + relaxation_side=relaxation_side, + f_x_expr=arg1**arg2, + shape=FunctionShape.CONVEX, + ) aux_var_map[Hashable(arg1, arg2, 'pow')] = (_aux_var, relaxation) setattr(parent_block.relaxations, 'rel' + str(counter), relaxation) counter.increment() return _aux_var -def _relax_concave_pow(arg1, arg2, aux_var_map, relaxation_side, degree_map, parent_block, counter): - _aux_var = _get_aux_var(parent_block, arg1 ** arg2) +def _relax_concave_pow( + arg1, arg2, aux_var_map, relaxation_side, degree_map, parent_block, counter +): + _aux_var = _get_aux_var(parent_block, arg1**arg2) arg1 = replace_sub_expression_with_aux_var(arg1, parent_block) degree_map[_aux_var] = 1 relaxation = PWUnivariateRelaxation() - relaxation.set_input(x=arg1, aux_var=_aux_var, relaxation_side=relaxation_side, f_x_expr=arg1 ** arg2, - shape=FunctionShape.CONCAVE) + relaxation.set_input( + x=arg1, + aux_var=_aux_var, + relaxation_side=relaxation_side, + f_x_expr=arg1**arg2, + shape=FunctionShape.CONCAVE, + ) aux_var_map[Hashable(arg1, arg2, 'pow')] = (_aux_var, relaxation) setattr(parent_block.relaxations, 'rel' + str(counter), relaxation) counter.increment() return _aux_var -def _relax_leaf_to_root_PowExpression(node, values, aux_var_map, degree_map, parent_block, relaxation_side_map, counter): +def _relax_leaf_to_root_PowExpression( + node, values, aux_var_map, degree_map, parent_block, relaxation_side_map, counter +): arg1, arg2 = values h = Hashable(arg1, arg2, 'pow') if h in aux_var_map: @@ -341,11 +418,17 @@ def _relax_leaf_to_root_PowExpression(node, values, aux_var_map, degree_map, par degree2 = degree_map[arg2] if degree2 == 0: if degree1 == 0: - res = arg1 ** arg2 + res = arg1**arg2 degree_map[res] = 0 return res if not is_constant(arg2): - logger.warning('Only constant exponents are supported: ' + str(arg1**arg2) + '\nReplacing ' + str(arg2) + ' with its value.') + logger.warning( + 'Only constant exponents are supported: ' + + str(arg1**arg2) + + '\nReplacing ' + + str(arg2) + + ' with its value.' + ) arg2 = pe.value(arg2) if arg2 == 1: return arg1 @@ -354,114 +437,214 @@ def _relax_leaf_to_root_PowExpression(node, values, aux_var_map, degree_map, par degree_map[res] = 0 return res elif arg2 == 2: - return _relax_quadratic(arg1=arg1, aux_var_map=aux_var_map, relaxation_side=relaxation_side_map[node], - degree_map=degree_map, parent_block=parent_block, counter=counter) + return _relax_quadratic( + arg1=arg1, + aux_var_map=aux_var_map, + relaxation_side=relaxation_side_map[node], + degree_map=degree_map, + parent_block=parent_block, + counter=counter, + ) elif arg2 >= 0: if arg2 == round(arg2): if arg2 % 2 == 0 or compute_float_bounds_on_expr(arg1)[0] >= 0: - return _relax_convex_pow(arg1=arg1, arg2=arg2, aux_var_map=aux_var_map, - relaxation_side=relaxation_side_map[node], degree_map=degree_map, - parent_block=parent_block, counter=counter) + return _relax_convex_pow( + arg1=arg1, + arg2=arg2, + aux_var_map=aux_var_map, + relaxation_side=relaxation_side_map[node], + degree_map=degree_map, + parent_block=parent_block, + counter=counter, + ) elif compute_float_bounds_on_expr(arg1)[1] <= 0: - return _relax_concave_pow(arg1=arg1, arg2=arg2, aux_var_map=aux_var_map, - relaxation_side=relaxation_side_map[node], degree_map=degree_map, - parent_block=parent_block, counter=counter) + return _relax_concave_pow( + arg1=arg1, + arg2=arg2, + aux_var_map=aux_var_map, + relaxation_side=relaxation_side_map[node], + degree_map=degree_map, + parent_block=parent_block, + counter=counter, + ) else: # reformulate arg1 ** arg2 as arg1 * arg1 ** (arg2 - 1) _new_relaxation_side_map = ComponentMap() _reformulated = arg1 * arg1 ** (arg2 - 1) - _new_relaxation_side_map[_reformulated] = relaxation_side_map[node] - res = _relax_expr(expr=_reformulated, aux_var_map=aux_var_map, parent_block=parent_block, - relaxation_side_map=_new_relaxation_side_map, counter=counter, - degree_map=degree_map) + _new_relaxation_side_map[_reformulated] = relaxation_side_map[ + node + ] + res = _relax_expr( + expr=_reformulated, + aux_var_map=aux_var_map, + parent_block=parent_block, + relaxation_side_map=_new_relaxation_side_map, + counter=counter, + degree_map=degree_map, + ) degree_map[res] = 1 return res else: if arg2 < 1: - return _relax_concave_pow(arg1=arg1, arg2=arg2, aux_var_map=aux_var_map, - relaxation_side=relaxation_side_map[node], degree_map=degree_map, - parent_block=parent_block, counter=counter) + return _relax_concave_pow( + arg1=arg1, + arg2=arg2, + aux_var_map=aux_var_map, + relaxation_side=relaxation_side_map[node], + degree_map=degree_map, + parent_block=parent_block, + counter=counter, + ) else: - return _relax_convex_pow(arg1=arg1, arg2=arg2, aux_var_map=aux_var_map, - relaxation_side=relaxation_side_map[node], degree_map=degree_map, - parent_block=parent_block, counter=counter) + return _relax_convex_pow( + arg1=arg1, + arg2=arg2, + aux_var_map=aux_var_map, + relaxation_side=relaxation_side_map[node], + degree_map=degree_map, + parent_block=parent_block, + counter=counter, + ) else: if arg2 == round(arg2): if compute_float_bounds_on_expr(arg1)[0] >= 0: - return _relax_convex_pow(arg1=arg1, arg2=arg2, aux_var_map=aux_var_map, - relaxation_side=relaxation_side_map[node], degree_map=degree_map, - parent_block=parent_block, counter=counter) + return _relax_convex_pow( + arg1=arg1, + arg2=arg2, + aux_var_map=aux_var_map, + relaxation_side=relaxation_side_map[node], + degree_map=degree_map, + parent_block=parent_block, + counter=counter, + ) elif compute_float_bounds_on_expr(arg1)[1] <= 0: if arg2 % 2 == 0: - return _relax_convex_pow(arg1=arg1, arg2=arg2, aux_var_map=aux_var_map, - relaxation_side=relaxation_side_map[node], degree_map=degree_map, - parent_block=parent_block, counter=counter) + return _relax_convex_pow( + arg1=arg1, + arg2=arg2, + aux_var_map=aux_var_map, + relaxation_side=relaxation_side_map[node], + degree_map=degree_map, + parent_block=parent_block, + counter=counter, + ) else: - return _relax_concave_pow(arg1=arg1, arg2=arg2, aux_var_map=aux_var_map, - relaxation_side=relaxation_side_map[node], degree_map=degree_map, - parent_block=parent_block, counter=counter) + return _relax_concave_pow( + arg1=arg1, + arg2=arg2, + aux_var_map=aux_var_map, + relaxation_side=relaxation_side_map[node], + degree_map=degree_map, + parent_block=parent_block, + counter=counter, + ) else: # reformulate arg1 ** arg2 as 1 / arg1 ** (-arg2) _new_relaxation_side_map = ComponentMap() _reformulated = 1 / (arg1 ** (-arg2)) - _new_relaxation_side_map[_reformulated] = relaxation_side_map[node] - res = _relax_expr(expr=_reformulated, aux_var_map=aux_var_map, parent_block=parent_block, - relaxation_side_map=_new_relaxation_side_map, counter=counter, - degree_map=degree_map) + _new_relaxation_side_map[_reformulated] = relaxation_side_map[ + node + ] + res = _relax_expr( + expr=_reformulated, + aux_var_map=aux_var_map, + parent_block=parent_block, + relaxation_side_map=_new_relaxation_side_map, + counter=counter, + degree_map=degree_map, + ) degree_map[res] = 1 return res else: assert compute_float_bounds_on_expr(arg1)[0] >= 0 - return _relax_convex_pow(arg1=arg1, arg2=arg2, aux_var_map=aux_var_map, - relaxation_side=relaxation_side_map[node], degree_map=degree_map, - parent_block=parent_block, counter=counter) + return _relax_convex_pow( + arg1=arg1, + arg2=arg2, + aux_var_map=aux_var_map, + relaxation_side=relaxation_side_map[node], + degree_map=degree_map, + parent_block=parent_block, + counter=counter, + ) elif degree1 == 0: if not is_constant(arg1): - logger.warning('Found {0} raised to a variable power. However, {0} does not appear to be constant (maybe ' - 'it is or depends on a mutable Param?). Replacing {0} with its value.'.format(str(arg1))) + logger.warning( + 'Found {0} raised to a variable power. However, {0} does not appear to be constant (maybe ' + 'it is or depends on a mutable Param?). Replacing {0} with its value.'.format( + str(arg1) + ) + ) arg1 = pe.value(arg1) if arg1 < 0: - raise ValueError('Cannot raise a negative base to a variable exponent: ' + str(arg1**arg2)) - return _relax_convex_pow(arg1=arg1, arg2=arg2, aux_var_map=aux_var_map, - relaxation_side=relaxation_side_map[node], degree_map=degree_map, - parent_block=parent_block, counter=counter, swap=True) + raise ValueError( + 'Cannot raise a negative base to a variable exponent: ' + + str(arg1**arg2) + ) + return _relax_convex_pow( + arg1=arg1, + arg2=arg2, + aux_var_map=aux_var_map, + relaxation_side=relaxation_side_map[node], + degree_map=degree_map, + parent_block=parent_block, + counter=counter, + swap=True, + ) else: assert compute_float_bounds_on_expr(arg1)[0] >= 0 _new_relaxation_side_map = ComponentMap() _reformulated = pe.exp(arg2 * pe.log(arg1)) _new_relaxation_side_map[_reformulated] = relaxation_side_map[node] - res = _relax_expr(expr=_reformulated, aux_var_map=aux_var_map, parent_block=parent_block, - relaxation_side_map=_new_relaxation_side_map, counter=counter, - degree_map=degree_map) + res = _relax_expr( + expr=_reformulated, + aux_var_map=aux_var_map, + parent_block=parent_block, + relaxation_side_map=_new_relaxation_side_map, + counter=counter, + degree_map=degree_map, + ) degree_map[res] = 1 return res -def _relax_leaf_to_root_SumExpression(node, values, aux_var_map, degree_map, parent_block, relaxation_side_map, counter): +def _relax_leaf_to_root_SumExpression( + node, values, aux_var_map, degree_map, parent_block, relaxation_side_map, counter +): res = sum(values) degree_map[res] = max([degree_map[arg] for arg in values]) return res -def _relax_leaf_to_root_NegationExpression(node, values, aux_var_map, degree_map, parent_block, relaxation_side_map, counter): +def _relax_leaf_to_root_NegationExpression( + node, values, aux_var_map, degree_map, parent_block, relaxation_side_map, counter +): arg = values[0] res = -arg degree_map[res] = degree_map[arg] return res -def _relax_leaf_to_root_sqrt(node, values, aux_var_map, degree_map, parent_block, relaxation_side_map, counter): +def _relax_leaf_to_root_sqrt( + node, values, aux_var_map, degree_map, parent_block, relaxation_side_map, counter +): arg = values[0] _new_relaxation_side_map = ComponentMap() _reformulated = arg**0.5 _new_relaxation_side_map[_reformulated] = relaxation_side_map[node] - res = _relax_expr(expr=_reformulated, aux_var_map=aux_var_map, parent_block=parent_block, - relaxation_side_map=_new_relaxation_side_map, counter=counter, - degree_map=degree_map) + res = _relax_expr( + expr=_reformulated, + aux_var_map=aux_var_map, + parent_block=parent_block, + relaxation_side_map=_new_relaxation_side_map, + counter=counter, + degree_map=degree_map, + ) degree_map[res] = 1 return res -def _relax_leaf_to_root_exp(node, values, aux_var_map, degree_map, parent_block, relaxation_side_map, counter): +def _relax_leaf_to_root_exp( + node, values, aux_var_map, degree_map, parent_block, relaxation_side_map, counter +): arg = values[0] degree = degree_map[arg] if degree == 0: @@ -481,15 +664,22 @@ def _relax_leaf_to_root_exp(node, values, aux_var_map, degree_map, parent_block, relaxation_side = relaxation_side_map[node] degree_map[_aux_var] = 1 relaxation = PWUnivariateRelaxation() - relaxation.set_input(x=arg, aux_var=_aux_var, relaxation_side=relaxation_side, f_x_expr=pe.exp(arg), - shape=FunctionShape.CONVEX) + relaxation.set_input( + x=arg, + aux_var=_aux_var, + relaxation_side=relaxation_side, + f_x_expr=pe.exp(arg), + shape=FunctionShape.CONVEX, + ) aux_var_map[id(arg), 'exp'] = (_aux_var, relaxation) - setattr(parent_block.relaxations, 'rel'+str(counter), relaxation) + setattr(parent_block.relaxations, 'rel' + str(counter), relaxation) counter.increment() return _aux_var -def _relax_leaf_to_root_log(node, values, aux_var_map, degree_map, parent_block, relaxation_side_map, counter): +def _relax_leaf_to_root_log( + node, values, aux_var_map, degree_map, parent_block, relaxation_side_map, counter +): arg = values[0] degree = degree_map[arg] if degree == 0: @@ -509,15 +699,22 @@ def _relax_leaf_to_root_log(node, values, aux_var_map, degree_map, parent_block, relaxation_side = relaxation_side_map[node] degree_map[_aux_var] = 1 relaxation = PWUnivariateRelaxation() - relaxation.set_input(x=arg, aux_var=_aux_var, relaxation_side=relaxation_side, f_x_expr=pe.log(arg), - shape=FunctionShape.CONCAVE) + relaxation.set_input( + x=arg, + aux_var=_aux_var, + relaxation_side=relaxation_side, + f_x_expr=pe.log(arg), + shape=FunctionShape.CONCAVE, + ) aux_var_map[id(arg), 'log'] = (_aux_var, relaxation) - setattr(parent_block.relaxations, 'rel'+str(counter), relaxation) + setattr(parent_block.relaxations, 'rel' + str(counter), relaxation) counter.increment() return _aux_var -def _relax_leaf_to_root_log10(node, values, aux_var_map, degree_map, parent_block, relaxation_side_map, counter): +def _relax_leaf_to_root_log10( + node, values, aux_var_map, degree_map, parent_block, relaxation_side_map, counter +): arg = values[0] degree = degree_map[arg] if degree == 0: @@ -537,15 +734,22 @@ def _relax_leaf_to_root_log10(node, values, aux_var_map, degree_map, parent_bloc relaxation_side = relaxation_side_map[node] degree_map[_aux_var] = 1 relaxation = PWUnivariateRelaxation() - relaxation.set_input(x=arg, aux_var=_aux_var, relaxation_side=relaxation_side, f_x_expr=pe.log10(arg), - shape=FunctionShape.CONCAVE) + relaxation.set_input( + x=arg, + aux_var=_aux_var, + relaxation_side=relaxation_side, + f_x_expr=pe.log10(arg), + shape=FunctionShape.CONCAVE, + ) aux_var_map[id(arg), 'log10'] = (_aux_var, relaxation) - setattr(parent_block.relaxations, 'rel'+str(counter), relaxation) + setattr(parent_block.relaxations, 'rel' + str(counter), relaxation) counter.increment() return _aux_var -def _relax_leaf_to_root_sin(node, values, aux_var_map, degree_map, parent_block, relaxation_side_map, counter): +def _relax_leaf_to_root_sin( + node, values, aux_var_map, degree_map, parent_block, relaxation_side_map, counter +): arg = values[0] degree = degree_map[arg] if degree == 0: @@ -567,12 +771,14 @@ def _relax_leaf_to_root_sin(node, values, aux_var_map, degree_map, parent_block, relaxation = PWSinRelaxation() relaxation.set_input(x=arg, aux_var=_aux_var, relaxation_side=relaxation_side) aux_var_map[id(arg), 'sin'] = (_aux_var, relaxation) - setattr(parent_block.relaxations, 'rel'+str(counter), relaxation) + setattr(parent_block.relaxations, 'rel' + str(counter), relaxation) counter.increment() return _aux_var -def _relax_leaf_to_root_cos(node, values, aux_var_map, degree_map, parent_block, relaxation_side_map, counter): +def _relax_leaf_to_root_cos( + node, values, aux_var_map, degree_map, parent_block, relaxation_side_map, counter +): arg = values[0] degree = degree_map[arg] if degree == 0: @@ -594,12 +800,14 @@ def _relax_leaf_to_root_cos(node, values, aux_var_map, degree_map, parent_block, relaxation = PWCosRelaxation() relaxation.set_input(x=arg, aux_var=_aux_var, relaxation_side=relaxation_side) aux_var_map[id(arg), 'cos'] = (_aux_var, relaxation) - setattr(parent_block.relaxations, 'rel'+str(counter), relaxation) + setattr(parent_block.relaxations, 'rel' + str(counter), relaxation) counter.increment() return _aux_var -def _relax_leaf_to_root_arctan(node, values, aux_var_map, degree_map, parent_block, relaxation_side_map, counter): +def _relax_leaf_to_root_arctan( + node, values, aux_var_map, degree_map, parent_block, relaxation_side_map, counter +): arg = values[0] degree = degree_map[arg] if degree == 0: @@ -621,12 +829,14 @@ def _relax_leaf_to_root_arctan(node, values, aux_var_map, degree_map, parent_blo relaxation = PWArctanRelaxation() relaxation.set_input(x=arg, aux_var=_aux_var, relaxation_side=relaxation_side) aux_var_map[id(arg), 'arctan'] = (_aux_var, relaxation) - setattr(parent_block.relaxations, 'rel'+str(counter), relaxation) + setattr(parent_block.relaxations, 'rel' + str(counter), relaxation) counter.increment() return _aux_var -def _relax_leaf_to_root_tan(node, values, aux_var_map, degree_map, parent_block, relaxation_side_map, counter): +def _relax_leaf_to_root_tan( + node, values, aux_var_map, degree_map, parent_block, relaxation_side_map, counter +): arg = values[0] degree = degree_map[arg] if degree == 0: @@ -646,22 +856,28 @@ def _relax_leaf_to_root_tan(node, values, aux_var_map, degree_map, parent_block, relaxation_side = relaxation_side_map[node] degree_map[_aux_var] = 1 - if arg.lb >=0 and arg.ub <= math.pi/2: + if arg.lb >= 0 and arg.ub <= math.pi / 2: relaxation = PWUnivariateRelaxation() relaxation.set_input( - x=arg, aux_var=_aux_var, shape=FunctionShape.CONVEX, - f_x_expr=pe.tan(arg), relaxation_side=relaxation_side + x=arg, + aux_var=_aux_var, + shape=FunctionShape.CONVEX, + f_x_expr=pe.tan(arg), + relaxation_side=relaxation_side, ) - elif arg.lb >= -math.pi/2 and arg.ub <= 0: + elif arg.lb >= -math.pi / 2 and arg.ub <= 0: relaxation = PWUnivariateRelaxation() relaxation.set_input( - x=arg, aux_var=_aux_var, shape=FunctionShape.CONCAVE, - f_x_expr=pe.tan(arg), relaxation_side=relaxation_side + x=arg, + aux_var=_aux_var, + shape=FunctionShape.CONCAVE, + f_x_expr=pe.tan(arg), + relaxation_side=relaxation_side, ) else: raise NotImplementedError('Use alpha-BB here') aux_var_map[id(arg), 'tan'] = (_aux_var, relaxation) - setattr(parent_block.relaxations, 'rel'+str(counter), relaxation) + setattr(parent_block.relaxations, 'rel' + str(counter), relaxation) counter.increment() return _aux_var @@ -677,35 +893,69 @@ def _relax_leaf_to_root_tan(node, values, aux_var_map, degree_map, parent_block, _unary_leaf_to_root_map['tan'] = _relax_leaf_to_root_tan -def _relax_leaf_to_root_UnaryFunctionExpression(node, values, aux_var_map, degree_map, parent_block, relaxation_side_map, counter): +def _relax_leaf_to_root_UnaryFunctionExpression( + node, values, aux_var_map, degree_map, parent_block, relaxation_side_map, counter +): if node.getname() in _unary_leaf_to_root_map: - return _unary_leaf_to_root_map[node.getname()](node=node, values=values, aux_var_map=aux_var_map, - degree_map=degree_map, parent_block=parent_block, - relaxation_side_map=relaxation_side_map, counter=counter) + return _unary_leaf_to_root_map[node.getname()]( + node=node, + values=values, + aux_var_map=aux_var_map, + degree_map=degree_map, + parent_block=parent_block, + relaxation_side_map=relaxation_side_map, + counter=counter, + ) else: raise NotImplementedError('Cannot automatically relax ' + str(node)) -def _relax_leaf_to_root_GeneralExpression(node, values, aux_var_map, degree_map, parent_block, relaxation_side_map, counter): +def _relax_leaf_to_root_GeneralExpression( + node, values, aux_var_map, degree_map, parent_block, relaxation_side_map, counter +): arg = values[0] return arg _relax_leaf_to_root_map = dict() -_relax_leaf_to_root_map[numeric_expr.ProductExpression] = _relax_leaf_to_root_ProductExpression +_relax_leaf_to_root_map[ + numeric_expr.ProductExpression +] = _relax_leaf_to_root_ProductExpression _relax_leaf_to_root_map[numeric_expr.SumExpression] = _relax_leaf_to_root_SumExpression -_relax_leaf_to_root_map[numeric_expr.LinearExpression] = _relax_leaf_to_root_SumExpression -_relax_leaf_to_root_map[numeric_expr.MonomialTermExpression] = _relax_leaf_to_root_ProductExpression -_relax_leaf_to_root_map[numeric_expr.NegationExpression] = _relax_leaf_to_root_NegationExpression +_relax_leaf_to_root_map[ + numeric_expr.LinearExpression +] = _relax_leaf_to_root_SumExpression +_relax_leaf_to_root_map[ + numeric_expr.MonomialTermExpression +] = _relax_leaf_to_root_ProductExpression +_relax_leaf_to_root_map[ + numeric_expr.NegationExpression +] = _relax_leaf_to_root_NegationExpression _relax_leaf_to_root_map[numeric_expr.PowExpression] = _relax_leaf_to_root_PowExpression -_relax_leaf_to_root_map[numeric_expr.DivisionExpression] = _relax_leaf_to_root_DivisionExpression -_relax_leaf_to_root_map[numeric_expr.UnaryFunctionExpression] = _relax_leaf_to_root_UnaryFunctionExpression -_relax_leaf_to_root_map[numeric_expr.NPV_ProductExpression] = _relax_leaf_to_root_ProductExpression -_relax_leaf_to_root_map[numeric_expr.NPV_SumExpression] = _relax_leaf_to_root_SumExpression -_relax_leaf_to_root_map[numeric_expr.NPV_NegationExpression] = _relax_leaf_to_root_NegationExpression -_relax_leaf_to_root_map[numeric_expr.NPV_PowExpression] = _relax_leaf_to_root_PowExpression -_relax_leaf_to_root_map[numeric_expr.NPV_DivisionExpression] = _relax_leaf_to_root_DivisionExpression -_relax_leaf_to_root_map[numeric_expr.NPV_UnaryFunctionExpression] = _relax_leaf_to_root_UnaryFunctionExpression +_relax_leaf_to_root_map[ + numeric_expr.DivisionExpression +] = _relax_leaf_to_root_DivisionExpression +_relax_leaf_to_root_map[ + numeric_expr.UnaryFunctionExpression +] = _relax_leaf_to_root_UnaryFunctionExpression +_relax_leaf_to_root_map[ + numeric_expr.NPV_ProductExpression +] = _relax_leaf_to_root_ProductExpression +_relax_leaf_to_root_map[ + numeric_expr.NPV_SumExpression +] = _relax_leaf_to_root_SumExpression +_relax_leaf_to_root_map[ + numeric_expr.NPV_NegationExpression +] = _relax_leaf_to_root_NegationExpression +_relax_leaf_to_root_map[ + numeric_expr.NPV_PowExpression +] = _relax_leaf_to_root_PowExpression +_relax_leaf_to_root_map[ + numeric_expr.NPV_DivisionExpression +] = _relax_leaf_to_root_DivisionExpression +_relax_leaf_to_root_map[ + numeric_expr.NPV_UnaryFunctionExpression +] = _relax_leaf_to_root_UnaryFunctionExpression _relax_leaf_to_root_map[_GeneralExpressionData] = _relax_leaf_to_root_GeneralExpression _relax_leaf_to_root_map[SimpleExpression] = _relax_leaf_to_root_GeneralExpression @@ -868,20 +1118,44 @@ def _relax_root_to_leaf_GeneralExpression(node, relaxation_side_map): _relax_root_to_leaf_map = dict() -_relax_root_to_leaf_map[numeric_expr.ProductExpression] = _relax_root_to_leaf_ProductExpression +_relax_root_to_leaf_map[ + numeric_expr.ProductExpression +] = _relax_root_to_leaf_ProductExpression _relax_root_to_leaf_map[numeric_expr.SumExpression] = _relax_root_to_leaf_SumExpression -_relax_root_to_leaf_map[numeric_expr.LinearExpression] = _relax_root_to_leaf_SumExpression -_relax_root_to_leaf_map[numeric_expr.MonomialTermExpression] = _relax_root_to_leaf_ProductExpression -_relax_root_to_leaf_map[numeric_expr.NegationExpression] = _relax_root_to_leaf_NegationExpression +_relax_root_to_leaf_map[ + numeric_expr.LinearExpression +] = _relax_root_to_leaf_SumExpression +_relax_root_to_leaf_map[ + numeric_expr.MonomialTermExpression +] = _relax_root_to_leaf_ProductExpression +_relax_root_to_leaf_map[ + numeric_expr.NegationExpression +] = _relax_root_to_leaf_NegationExpression _relax_root_to_leaf_map[numeric_expr.PowExpression] = _relax_root_to_leaf_PowExpression -_relax_root_to_leaf_map[numeric_expr.DivisionExpression] = _relax_root_to_leaf_DivisionExpression -_relax_root_to_leaf_map[numeric_expr.UnaryFunctionExpression] = _relax_root_to_leaf_UnaryFunctionExpression -_relax_root_to_leaf_map[numeric_expr.NPV_ProductExpression] = _relax_root_to_leaf_ProductExpression -_relax_root_to_leaf_map[numeric_expr.NPV_SumExpression] = _relax_root_to_leaf_SumExpression -_relax_root_to_leaf_map[numeric_expr.NPV_NegationExpression] = _relax_root_to_leaf_NegationExpression -_relax_root_to_leaf_map[numeric_expr.NPV_PowExpression] = _relax_root_to_leaf_PowExpression -_relax_root_to_leaf_map[numeric_expr.NPV_DivisionExpression] = _relax_root_to_leaf_DivisionExpression -_relax_root_to_leaf_map[numeric_expr.NPV_UnaryFunctionExpression] = _relax_root_to_leaf_UnaryFunctionExpression +_relax_root_to_leaf_map[ + numeric_expr.DivisionExpression +] = _relax_root_to_leaf_DivisionExpression +_relax_root_to_leaf_map[ + numeric_expr.UnaryFunctionExpression +] = _relax_root_to_leaf_UnaryFunctionExpression +_relax_root_to_leaf_map[ + numeric_expr.NPV_ProductExpression +] = _relax_root_to_leaf_ProductExpression +_relax_root_to_leaf_map[ + numeric_expr.NPV_SumExpression +] = _relax_root_to_leaf_SumExpression +_relax_root_to_leaf_map[ + numeric_expr.NPV_NegationExpression +] = _relax_root_to_leaf_NegationExpression +_relax_root_to_leaf_map[ + numeric_expr.NPV_PowExpression +] = _relax_root_to_leaf_PowExpression +_relax_root_to_leaf_map[ + numeric_expr.NPV_DivisionExpression +] = _relax_root_to_leaf_DivisionExpression +_relax_root_to_leaf_map[ + numeric_expr.NPV_UnaryFunctionExpression +] = _relax_root_to_leaf_UnaryFunctionExpression _relax_root_to_leaf_map[_GeneralExpressionData] = _relax_root_to_leaf_GeneralExpression _relax_root_to_leaf_map[SimpleExpression] = _relax_root_to_leaf_GeneralExpression @@ -892,7 +1166,10 @@ class _FactorableRelaxationVisitor(ExpressionValueVisitor): auxiliary variables, and relaxations relating the auxilliary variables to the original variables. """ - def __init__(self, aux_var_map, parent_block, relaxation_side_map, counter, degree_map): + + def __init__( + self, aux_var_map, parent_block, relaxation_side_map, counter, degree_map + ): self.aux_var_map = aux_var_map self.parent_block = parent_block self.relaxation_side_map = relaxation_side_map @@ -901,11 +1178,20 @@ def __init__(self, aux_var_map, parent_block, relaxation_side_map, counter, degr def visit(self, node, values): if node.__class__ in _relax_leaf_to_root_map: - res = _relax_leaf_to_root_map[node.__class__](node, values, self.aux_var_map, self.degree_map, - self.parent_block, self.relaxation_side_map, self.counter) + res = _relax_leaf_to_root_map[node.__class__]( + node, + values, + self.aux_var_map, + self.degree_map, + self.parent_block, + self.relaxation_side_map, + self.counter, + ) return res else: - raise NotImplementedError('Cannot relax an expression of type ' + str(type(node))) + raise NotImplementedError( + 'Cannot relax an expression of type ' + str(type(node)) + ) def visiting_potential_leaf(self, node): if node.__class__ in nonpyomo_leaf_types: @@ -926,7 +1212,9 @@ def visiting_potential_leaf(self, node): if node.__class__ in _relax_root_to_leaf_map: _relax_root_to_leaf_map[node.__class__](node, self.relaxation_side_map) else: - raise NotImplementedError('Cannot relax an expression of type ' + str(type(node))) + raise NotImplementedError( + 'Cannot relax an expression of type ' + str(type(node)) + ) return False, None @@ -949,10 +1237,16 @@ def _get_prefix_notation(expr): return tuple(res) -def _relax_expr(expr, aux_var_map, parent_block, relaxation_side_map, counter, degree_map): - visitor = _FactorableRelaxationVisitor(aux_var_map=aux_var_map, parent_block=parent_block, - relaxation_side_map=relaxation_side_map, counter=counter, - degree_map=degree_map) +def _relax_expr( + expr, aux_var_map, parent_block, relaxation_side_map, counter, degree_map +): + visitor = _FactorableRelaxationVisitor( + aux_var_map=aux_var_map, + parent_block=parent_block, + relaxation_side_map=relaxation_side_map, + counter=counter, + degree_map=degree_map, + ) new_expr = visitor.dfs_postorder_stack(expr) return new_expr @@ -961,9 +1255,10 @@ def _relax_split_expr( expr: ExpressionBase, aux_var_map: MutableMapping[ Tuple, - Tuple[NumericValue, - Union[BaseRelaxationData, - Tuple[BaseRelaxationData, BaseRelaxationData]]] + Tuple[ + NumericValue, + Union[BaseRelaxationData, Tuple[BaseRelaxationData, BaseRelaxationData]], + ], ], parent_block: _BlockData, relaxation_side_map: MutableMapping[NumericValue, RelaxationSide], @@ -1003,15 +1298,19 @@ def _relax_split_expr( else: shape = FunctionShape.CONCAVE relaxation.set_input( - x=vlist[0], aux_var=new_expr, relaxation_side=relaxation_side, - f_x_expr=expr, shape=shape, + x=vlist[0], + aux_var=new_expr, + relaxation_side=relaxation_side, + f_x_expr=expr, + shape=shape, ) aux_var_map[pn] = (new_expr, relaxation) setattr(parent_block.relaxations, 'rel' + str(counter), relaxation) counter.increment() degree_map[new_expr] = 1 - elif ((is_convex and relaxation_side == RelaxationSide.UNDER) - or (is_concave and relaxation_side == RelaxationSide.OVER)): + elif (is_convex and relaxation_side == RelaxationSide.UNDER) or ( + is_concave and relaxation_side == RelaxationSide.OVER + ): pn = _get_prefix_notation(expr) if pn in aux_var_map: new_expr, (underestimator, overestimator) = aux_var_map[pn] @@ -1019,9 +1318,8 @@ def _relax_split_expr( new_expr, underestimator, overestimator = None, None, None if new_expr is None: new_expr = _get_aux_var(parent_block, expr) - if ( - (is_convex and underestimator is None) - or (is_concave and overestimator is None) + if (is_convex and underestimator is None) or ( + is_concave and overestimator is None ): relaxation = MultivariateRelaxation() if is_convex: @@ -1030,9 +1328,7 @@ def _relax_split_expr( else: shape = FunctionShape.CONCAVE overestimator = relaxation - relaxation.set_input( - aux_var=new_expr, shape=shape, f_x_expr=expr, - ) + relaxation.set_input(aux_var=new_expr, shape=shape, f_x_expr=expr) aux_var_map[pn] = (new_expr, (underestimator, overestimator)) setattr(parent_block.relaxations, 'rel' + str(counter), relaxation) counter.increment() @@ -1041,8 +1337,14 @@ def _relax_split_expr( all_vars_bounded and len(vlist) <= max_vars_per_alpha_bb and ( - (relaxation_side == RelaxationSide.UNDER and min_eig >= -abs(max_eigenvalue_for_alpha_bb)) - or (relaxation_side == RelaxationSide.OVER and max_eig <= abs(max_eigenvalue_for_alpha_bb)) + ( + relaxation_side == RelaxationSide.UNDER + and min_eig >= -abs(max_eigenvalue_for_alpha_bb) + ) + or ( + relaxation_side == RelaxationSide.OVER + and max_eig <= abs(max_eigenvalue_for_alpha_bb) + ) ) ): pn = _get_prefix_notation(expr) @@ -1052,9 +1354,8 @@ def _relax_split_expr( new_expr, underestimator, overestimator = None, None, None if new_expr is None: new_expr = _get_aux_var(parent_block, expr) - if ( - (relaxation_side == RelaxationSide.UNDER and underestimator is None) - or (relaxation_side == RelaxationSide.OVER and overestimator is None) + if (relaxation_side == RelaxationSide.UNDER and underestimator is None) or ( + relaxation_side == RelaxationSide.OVER and overestimator is None ): relaxation = AlphaBBRelaxation() relaxation.set_input( @@ -1072,11 +1373,13 @@ def _relax_split_expr( counter.increment() degree_map[new_expr] = 1 else: - visitor = _FactorableRelaxationVisitor(aux_var_map=aux_var_map, - parent_block=parent_block, - relaxation_side_map=relaxation_side_map, - counter=counter, - degree_map=degree_map) + visitor = _FactorableRelaxationVisitor( + aux_var_map=aux_var_map, + parent_block=parent_block, + relaxation_side_map=relaxation_side_map, + counter=counter, + degree_map=degree_map, + ) new_expr = visitor.dfs_postorder_stack(expr) return new_expr @@ -1085,9 +1388,10 @@ def _relax_expr_with_convexity_check( orig_expr: ExpressionBase, aux_var_map: MutableMapping[ Tuple, - Tuple[NumericValue, - Union[BaseRelaxationData, - Tuple[BaseRelaxationData, BaseRelaxationData]]] + Tuple[ + NumericValue, + Union[BaseRelaxationData, Tuple[BaseRelaxationData, BaseRelaxationData]], + ], ], parent_block: _BlockData, relaxation_side_map: MutableMapping[NumericValue, RelaxationSide], @@ -1118,7 +1422,9 @@ def _relax_expr_with_convexity_check( ) res_list.append(tmp_res) linking_expr = res_list[0] - res_list[1] - linking_repn = generate_standard_repn(linking_expr, compute_values=False, quadratic=True) + linking_repn = generate_standard_repn( + linking_expr, compute_values=False, quadratic=True + ) linking_expr = linking_repn.to_expression() if is_constant(linking_expr): assert value(linking_expr) == 0 @@ -1177,10 +1483,10 @@ def relax( The types of pyomo components that should be checked for constraints to be relaxed. The default is (Block, Disjunct). in_place: bool, optional - If False (default=False), model will be cloned, and the clone will be relaxed. + If False (default=False), model will be cloned, and the clone will be relaxed. If True, then model will be modified in place. use_fbbt: bool, optional - If True (default=True), then FBBT will be used to tighten variable bounds. If False, + If True (default=True), then FBBT will be used to tighten variable bounds. If False, FBBT will not be used. fbbt_options: dict, optional The options to pass to the call to fbbt. See pyomo.contrib.fbbt.fbbt.fbbt for details. @@ -1234,7 +1540,9 @@ def relax( counter_dict = dict() degree_map = ComponentMap() - for c in nonrelaxation_component_data_objects(m, ctype=Constraint, active=True, descend_into=descend_into, sort=True): + for c in nonrelaxation_component_data_objects( + m, ctype=Constraint, active=True, descend_into=descend_into, sort=True + ): body_degree = polynomial_degree(c.body) if body_degree is not None: if body_degree <= 1: @@ -1247,7 +1555,9 @@ def relax( elif c.upper is not None: relaxation_side = RelaxationSide.UNDER else: - raise ValueError('Encountered a constraint without a lower or an upper bound: ' + str(c)) + raise ValueError( + 'Encountered a constraint without a lower or an upper bound: ' + str(c) + ) parent_block = c.parent_block() @@ -1264,7 +1574,11 @@ def relax( assert len(repn.quadratic_vars) == 0 assert repn.nonlinear_expr is not None if len(repn.linear_vars) > 0: - new_body = numeric_expr.LinearExpression(constant=repn.constant, linear_coefs=repn.linear_coefs, linear_vars=repn.linear_vars) + new_body = numeric_expr.LinearExpression( + constant=repn.constant, + linear_coefs=repn.linear_coefs, + linear_vars=repn.linear_vars, + ) else: new_body = repn.constant @@ -1273,15 +1587,21 @@ def relax( if not use_alpha_bb: new_body += _relax_expr( - expr=repn.nonlinear_expr, aux_var_map=aux_var_map, - parent_block=parent_block, relaxation_side_map=relaxation_side_map, - counter=counter, degree_map=degree_map + expr=repn.nonlinear_expr, + aux_var_map=aux_var_map, + parent_block=parent_block, + relaxation_side_map=relaxation_side_map, + counter=counter, + degree_map=degree_map, ) else: new_body += _relax_expr_with_convexity_check( - orig_expr=repn.nonlinear_expr, aux_var_map=aux_var_map, - parent_block=parent_block, relaxation_side_map=relaxation_side_map, - counter=counter, degree_map=degree_map, + orig_expr=repn.nonlinear_expr, + aux_var_map=aux_var_map, + parent_block=parent_block, + relaxation_side_map=relaxation_side_map, + counter=counter, + degree_map=degree_map, perform_expression_simplification=perform_expression_simplification, eigenvalue_bounder=eigenvalue_bounder, max_vars_per_alpha_bb=max_vars_per_alpha_bb, @@ -1297,7 +1617,9 @@ def relax( else: parent_block.del_component(c) - for c in nonrelaxation_component_data_objects(m, ctype=pe.Objective, active=True, descend_into=descend_into, sort=True): + for c in nonrelaxation_component_data_objects( + m, ctype=pe.Objective, active=True, descend_into=descend_into, sort=True + ): degree = polynomial_degree(c.expr) if degree is not None: if degree <= 1: @@ -1308,7 +1630,9 @@ def relax( elif c.sense == pe.maximize: relaxation_side = RelaxationSide.OVER else: - raise ValueError('Encountered an objective with an unrecognized sense: ' + str(c)) + raise ValueError( + 'Encountered an objective with an unrecognized sense: ' + str(c) + ) parent_block = c.parent_block() @@ -1328,7 +1652,11 @@ def relax( assert len(repn.quadratic_vars) == 0 assert repn.nonlinear_expr is not None if len(repn.linear_vars) > 0: - new_body = numeric_expr.LinearExpression(constant=repn.constant, linear_coefs=repn.linear_coefs, linear_vars=repn.linear_vars) + new_body = numeric_expr.LinearExpression( + constant=repn.constant, + linear_coefs=repn.linear_coefs, + linear_vars=repn.linear_vars, + ) else: new_body = repn.constant @@ -1337,15 +1665,21 @@ def relax( if not use_alpha_bb: new_body += _relax_expr( - expr=repn.nonlinear_expr, aux_var_map=aux_var_map, - parent_block=parent_block, relaxation_side_map=relaxation_side_map, - counter=counter, degree_map=degree_map + expr=repn.nonlinear_expr, + aux_var_map=aux_var_map, + parent_block=parent_block, + relaxation_side_map=relaxation_side_map, + counter=counter, + degree_map=degree_map, ) else: new_body += _relax_expr_with_convexity_check( - orig_expr=repn.nonlinear_expr, aux_var_map=aux_var_map, - parent_block=parent_block, relaxation_side_map=relaxation_side_map, - counter=counter, degree_map=degree_map, + orig_expr=repn.nonlinear_expr, + aux_var_map=aux_var_map, + parent_block=parent_block, + relaxation_side_map=relaxation_side_map, + counter=counter, + degree_map=degree_map, perform_expression_simplification=perform_expression_simplification, eigenvalue_bounder=eigenvalue_bounder, max_vars_per_alpha_bb=max_vars_per_alpha_bb, diff --git a/pyomo/contrib/coramin/relaxations/copy_relaxation.py b/pyomo/contrib/coramin/relaxations/copy_relaxation.py index 832eabc8691..a4e860c99c9 100644 --- a/pyomo/contrib/coramin/relaxations/copy_relaxation.py +++ b/pyomo/contrib/coramin/relaxations/copy_relaxation.py @@ -1,8 +1,18 @@ from .mccormick import PWMcCormickRelaxationData, PWMcCormickRelaxation -from .univariate import PWXSquaredRelaxationData, PWUnivariateRelaxationData, PWArctanRelaxationData, \ - PWCosRelaxationData, PWSinRelaxationData -from .univariate import PWXSquaredRelaxation, PWUnivariateRelaxation, PWArctanRelaxation, \ - PWCosRelaxation, PWSinRelaxation +from .univariate import ( + PWXSquaredRelaxationData, + PWUnivariateRelaxationData, + PWArctanRelaxationData, + PWCosRelaxationData, + PWSinRelaxationData, +) +from .univariate import ( + PWXSquaredRelaxation, + PWUnivariateRelaxation, + PWArctanRelaxation, + PWCosRelaxation, + PWSinRelaxation, +) from .alphabb import AlphaBBRelaxation, AlphaBBRelaxationData from .multivariate import MultivariateRelaxationData, MultivariateRelaxation from pyomo.core.expr.visitor import replace_expressions @@ -32,36 +42,54 @@ def copy_relaxation_with_local_data(rel, old_var_to_new_var_map): new_x = old_var_to_new_var_map[id(rel.get_rhs_vars()[0])] new_aux_var = old_var_to_new_var_map[id(rel.get_aux_var())] new_rel = PWXSquaredRelaxation(concrete=True) - new_rel.set_input(x=new_x, aux_var=new_aux_var, pw_repn=rel._pw_repn, - use_linear_relaxation=rel.use_linear_relaxation, - relaxation_side=rel.relaxation_side) + new_rel.set_input( + x=new_x, + aux_var=new_aux_var, + pw_repn=rel._pw_repn, + use_linear_relaxation=rel.use_linear_relaxation, + relaxation_side=rel.relaxation_side, + ) elif isinstance(rel, PWArctanRelaxationData): new_x = old_var_to_new_var_map[id(rel.get_rhs_vars()[0])] new_aux_var = old_var_to_new_var_map[id(rel.get_aux_var())] new_rel = PWArctanRelaxation(concrete=True) - new_rel.set_input(x=new_x, aux_var=new_aux_var, pw_repn=rel._pw_repn, - relaxation_side=rel.relaxation_side, - use_linear_relaxation=rel.use_linear_relaxation) + new_rel.set_input( + x=new_x, + aux_var=new_aux_var, + pw_repn=rel._pw_repn, + relaxation_side=rel.relaxation_side, + use_linear_relaxation=rel.use_linear_relaxation, + ) elif isinstance(rel, PWSinRelaxationData): new_x = old_var_to_new_var_map[id(rel.get_rhs_vars()[0])] new_aux_var = old_var_to_new_var_map[id(rel.get_aux_var())] new_rel = PWSinRelaxation(concrete=True) - new_rel.set_input(x=new_x, aux_var=new_aux_var, pw_repn=rel._pw_repn, - relaxation_side=rel.relaxation_side, - use_linear_relaxation=rel.use_linear_relaxation) + new_rel.set_input( + x=new_x, + aux_var=new_aux_var, + pw_repn=rel._pw_repn, + relaxation_side=rel.relaxation_side, + use_linear_relaxation=rel.use_linear_relaxation, + ) elif isinstance(rel, PWCosRelaxationData): new_x = old_var_to_new_var_map[id(rel.get_rhs_vars()[0])] new_aux_var = old_var_to_new_var_map[id(rel.get_aux_var())] new_rel = PWCosRelaxation(concrete=True) - new_rel.set_input(x=new_x, aux_var=new_aux_var, pw_repn=rel._pw_repn, - relaxation_side=rel.relaxation_side, - use_linear_relaxation=rel.use_linear_relaxation) + new_rel.set_input( + x=new_x, + aux_var=new_aux_var, + pw_repn=rel._pw_repn, + relaxation_side=rel.relaxation_side, + use_linear_relaxation=rel.use_linear_relaxation, + ) elif isinstance(rel, PWUnivariateRelaxationData): new_x = old_var_to_new_var_map[id(rel.get_rhs_vars()[0])] new_aux_var = old_var_to_new_var_map[id(rel.get_aux_var())] - new_f_x_expr = replace_expressions(rel.get_rhs_expr(), - substitution_map=old_var_to_new_var_map, - remove_named_expressions=True) + new_f_x_expr = replace_expressions( + rel.get_rhs_expr(), + substitution_map=old_var_to_new_var_map, + remove_named_expressions=True, + ) new_rel = PWUnivariateRelaxation(concrete=True) if rel.is_rhs_convex(): shape = FunctionShape.CONVEX @@ -69,10 +97,15 @@ def copy_relaxation_with_local_data(rel, old_var_to_new_var_map): shape = FunctionShape.CONCAVE else: shape = FunctionShape.UNKNOWN - new_rel.set_input(x=new_x, aux_var=new_aux_var, shape=shape, - f_x_expr=new_f_x_expr, pw_repn=rel._pw_repn, - relaxation_side=rel.relaxation_side, - use_linear_relaxation=rel.use_linear_relaxation) + new_rel.set_input( + x=new_x, + aux_var=new_aux_var, + shape=shape, + f_x_expr=new_f_x_expr, + pw_repn=rel._pw_repn, + relaxation_side=rel.relaxation_side, + use_linear_relaxation=rel.use_linear_relaxation, + ) elif isinstance(rel, PWMcCormickRelaxationData): rhs_vars = rel.get_rhs_vars() old_x1 = rhs_vars[0] @@ -81,14 +114,18 @@ def copy_relaxation_with_local_data(rel, old_var_to_new_var_map): new_x2 = old_var_to_new_var_map[id(old_x2)] new_aux_var = old_var_to_new_var_map[id(rel.get_aux_var())] new_rel = PWMcCormickRelaxation(concrete=True) - new_rel.set_input(x1=new_x1, x2=new_x2, aux_var=new_aux_var, - relaxation_side=rel.relaxation_side) + new_rel.set_input( + x1=new_x1, + x2=new_x2, + aux_var=new_aux_var, + relaxation_side=rel.relaxation_side, + ) elif isinstance(rel, AlphaBBRelaxationData): new_aux_var = old_var_to_new_var_map[id(rel.get_aux_var())] new_f_x_expr = replace_expressions( rel.get_rhs_expr(), substitution_map=old_var_to_new_var_map, - remove_named_expressions=True + remove_named_expressions=True, ) new_rel = AlphaBBRelaxation(concrete=True) new_rel.set_input( @@ -107,12 +144,18 @@ def copy_relaxation_with_local_data(rel, old_var_to_new_var_map): shape = FunctionShape.CONCAVE else: shape = FunctionShape.UNKNOWN - new_f_x_expr = replace_expressions(rel.get_rhs_expr(), - substitution_map=old_var_to_new_var_map, - remove_named_expressions=True) + new_f_x_expr = replace_expressions( + rel.get_rhs_expr(), + substitution_map=old_var_to_new_var_map, + remove_named_expressions=True, + ) new_rel = MultivariateRelaxation(concrete=True) - new_rel.set_input(aux_var=new_aux_var, shape=shape, f_x_expr=new_f_x_expr, - use_linear_relaxation=rel.use_linear_relaxation) + new_rel.set_input( + aux_var=new_aux_var, + shape=shape, + f_x_expr=new_f_x_expr, + use_linear_relaxation=rel.use_linear_relaxation, + ) else: raise ValueError('Unrecognized relaxation: {0}'.format(str(type(rel)))) diff --git a/pyomo/contrib/coramin/relaxations/custom_block.py b/pyomo/contrib/coramin/relaxations/custom_block.py index 9fed4e2f70c..06f16980072 100644 --- a/pyomo/contrib/coramin/relaxations/custom_block.py +++ b/pyomo/contrib/coramin/relaxations/custom_block.py @@ -1,6 +1,7 @@ import sys from pyomo.core.base.block import Block from pyomo.core.base.indexed_component import UnindexedComponent_set + # ToDo: documentation # ToDo: passing of kwargs down to the data object # ToDo: figure out if the setattr's are necessary in the decorator @@ -15,6 +16,7 @@ # ToDo: Document this custom block code with an example ''' + class _IndexedCustomBlockMeta(type): """Metaclass for creating an indexed block with a custom block data type.""" @@ -22,13 +24,16 @@ class _IndexedCustomBlockMeta(type): def __new__(meta, name, bases, dct): def __init__(self, *args, **kwargs): bases[0].__init__(self, *args, **kwargs) + dct["__init__"] = __init__ return type.__new__(meta, name, bases, dct) + class _ScalarCustomBlockMeta(type): '''Metaclass used to create a scalar block with a custom block data type ''' + def __new__(meta, name, bases, dct): def __init__(self, *args, **kwargs): # bases[0] is the custom block data object @@ -36,22 +41,24 @@ def __init__(self, *args, **kwargs): # bases[1] is the custom block object that # is used for declaration bases[1].__init__(self, *args, **kwargs) + dct["__init__"] = __init__ return type.__new__(meta, name, bases, dct) + class CustomBlock(Block): - ''' This CustomBlock is the base class that allows + '''This CustomBlock is the base class that allows for easy creation of specialized derived blocks ''' + def __new__(cls, *args, **kwds): - if cls.__name__.startswith('_Indexed') or \ - cls.__name__.startswith('_Scalar'): + if cls.__name__.startswith('_Indexed') or cls.__name__.startswith('_Scalar'): # we are entering here the second time (recursive) # therefore, we need to create what we have return super(CustomBlock, cls).__new__(cls) - if not args or (args[0] is UnindexedComponent_set and len(args)==1): + if not args or (args[0] is UnindexedComponent_set and len(args) == 1): bname = "_Scalar{}".format(cls.__name__) - n = _ScalarCustomBlockMeta(bname, (cls._ComponentDataClass, cls),{}) + n = _ScalarCustomBlockMeta(bname, (cls._ComponentDataClass, cls), {}) return n.__new__(n) else: bname = "_Indexed{}".format(cls.__name__) @@ -60,24 +67,27 @@ def __new__(cls, *args, **kwds): def declare_custom_block(name): - ''' Decorator to declare the custom component + '''Decorator to declare the custom component that goes along with a custom block data @declare_custom_block(name=FooBlock) class FooBlockData(_BlockData): # custom block data class ''' + def proc_dec(cls): # this is the decorator function that # creates the block component class c = type( - name, # name of new class - (CustomBlock,), # base classes - {"__module__": cls.__module__, "_ComponentDataClass": cls}) # magic to fix the module + name, # name of new class + (CustomBlock,), # base classes + {"__module__": cls.__module__, "_ComponentDataClass": cls}, + ) # magic to fix the module # are these necessary? setattr(sys.modules[cls.__module__], name, c) setattr(cls, '_orig_name', name) setattr(cls, '_orig_module', cls.__module__) return cls + return proc_dec diff --git a/pyomo/contrib/coramin/relaxations/hessian.py b/pyomo/contrib/coramin/relaxations/hessian.py index 3e0a98668da..1bf3bdcedb1 100644 --- a/pyomo/contrib/coramin/relaxations/hessian.py +++ b/pyomo/contrib/coramin/relaxations/hessian.py @@ -31,14 +31,14 @@ def _determinant(mat): else: i = 0 det = 0 - next_rows = np.array(list(range(i+1, nrows)), dtype=int) + next_rows = np.array(list(range(i + 1, nrows)), dtype=int) for j in range(nrows): next_cols = [k for k in range(j)] - next_cols.extend(k for k in range(j+1, nrows)) + next_cols.extend(k for k in range(j + 1, nrows)) next_cols = np.array(next_cols, dtype=int) next_mat = mat[next_rows, :] next_mat = next_mat[:, next_cols] - det += (-1)**(i + j) * mat[i, j] * _determinant(next_mat) + det += (-1) ** (i + j) * mat[i, j] * _determinant(next_mat) return simplify_expr(det) @@ -112,19 +112,21 @@ def formulate_eigenvalue_relaxation(self, sense=pe.minimize): if rel_ub is None or orig_ub < rel_ub: rel_v.setub(orig_ub) from .iterators import relaxation_data_objects - for b in relaxation_data_objects(self._eigenvalue_relaxation, descend_into=True, active=True): + + for b in relaxation_data_objects( + self._eigenvalue_relaxation, descend_into=True, active=True + ): b.rebuild() self._eigenvalue_relaxation.obj.sense = sense return self._eigenvalue_relaxation m = self.formulate_eigenvalue_problem(sense=sense) all_vars = list( - ComponentSet( - m.component_data_objects(pe.Var, descend_into=True) - ) + ComponentSet(m.component_data_objects(pe.Var, descend_into=True)) ) tmp_name = unique_component_name(m, "all_vars") setattr(m, tmp_name, all_vars) from .auto_relax import relax + relaxation = relax(m, in_place=False) new_vars = getattr(relaxation, "all_vars") self._orig_to_relaxation_vars = pe.ComponentMap(zip(all_vars, new_vars)) diff --git a/pyomo/contrib/coramin/relaxations/mccormick.py b/pyomo/contrib/coramin/relaxations/mccormick.py index fc82dbefb91..68589c92b7f 100644 --- a/pyomo/contrib/coramin/relaxations/mccormick.py +++ b/pyomo/contrib/coramin/relaxations/mccormick.py @@ -9,12 +9,15 @@ from pyomo.core.base.constraint import IndexedConstraint from pyomo.core.expr.numeric_expr import LinearExpression from typing import Optional, Dict, Sequence + pe = pyo logger = logging.getLogger(__name__) -def _build_pw_mccormick_relaxation(b, x1, x2, aux_var, x1_pts, relaxation_side=RelaxationSide.BOTH, safety_tol=1e-10): +def _build_pw_mccormick_relaxation( + b, x1, x2, aux_var, x1_pts, relaxation_side=RelaxationSide.BOTH, safety_tol=1e-10 +): """ This function creates piecewise envelopes to relax "aux_var = x1*x2". Note that the partitioning is done on "x1" only. This is the "nf4r" from Gounaris, Misener, and Floudas (2009). @@ -45,11 +48,11 @@ def _build_pw_mccormick_relaxation(b, x1, x2, aux_var, x1_pts, relaxation_side=R check_var_pts(x2) if x1.is_fixed() and x2.is_fixed(): - b.x1_x2_fixed_eq = pyo.Constraint(expr= aux_var == pyo.value(x1) * pyo.value(x2)) + b.x1_x2_fixed_eq = pyo.Constraint(expr=aux_var == pyo.value(x1) * pyo.value(x2)) elif x1.is_fixed(): - b.x1_fixed_eq = pyo.Constraint(expr= aux_var == pyo.value(x1) * x2) + b.x1_fixed_eq = pyo.Constraint(expr=aux_var == pyo.value(x1) * x2) elif x2.is_fixed(): - b.x2_fixed_eq = pyo.Constraint(expr= aux_var == x1 * pyo.value(x2)) + b.x2_fixed_eq = pyo.Constraint(expr=aux_var == x1 * pyo.value(x2)) else: # create the lambda_ variables (binaries for the pw representation) b.interval_set = pyo.Set(initialize=range(1, len(x1_pts))) @@ -59,14 +62,22 @@ def _build_pw_mccormick_relaxation(b, x1, x2, aux_var, x1_pts, relaxation_side=R b.delta_x2 = pyo.Var(b.interval_set, bounds=(0, None)) # create the "sos1" constraint - b.lambda_sos1 = pyo.Constraint(expr=sum(b.lambda_[n] for n in b.interval_set) == 1.0) + b.lambda_sos1 = pyo.Constraint( + expr=sum(b.lambda_[n] for n in b.interval_set) == 1.0 + ) # create the x1 interval constraints - b.x1_interval_lb = pyo.Constraint(expr=sum(x1_pts[n - 1] * b.lambda_[n] for n in b.interval_set) <= x1) - b.x1_interval_ub = pyo.Constraint(expr=x1 <= sum(x1_pts[n] * b.lambda_[n] for n in b.interval_set)) + b.x1_interval_lb = pyo.Constraint( + expr=sum(x1_pts[n - 1] * b.lambda_[n] for n in b.interval_set) <= x1 + ) + b.x1_interval_ub = pyo.Constraint( + expr=x1 <= sum(x1_pts[n] * b.lambda_[n] for n in b.interval_set) + ) # create the x2 constraints - b.x2_con = pyo.Constraint(expr=x2 == x2_lb + sum(b.delta_x2[n] for n in b.interval_set)) + b.x2_con = pyo.Constraint( + expr=x2 == x2_lb + sum(b.delta_x2[n] for n in b.interval_set) + ) def delta_x2n_ub_rule(m, n): return b.delta_x2[n] <= (x2_ub - x2_lb) * b.lambda_[n] @@ -74,15 +85,47 @@ def delta_x2n_ub_rule(m, n): b.delta_x2n_ub = pyo.Constraint(b.interval_set, rule=delta_x2n_ub_rule) # create the relaxation constraints - if relaxation_side == RelaxationSide.UNDER or relaxation_side == RelaxationSide.BOTH: - b.aux_var_lb1 = pyo.Constraint(expr=(aux_var >= x2_ub * x1 + sum(x1_pts[n] * b.delta_x2[n] for n in b.interval_set) - - (x2_ub - x2_lb) * sum(x1_pts[n] * b.lambda_[n] for n in b.interval_set) - safety_tol)) - b.aux_var_lb2 = pyo.Constraint(expr=aux_var >= x2_lb * x1 + sum(x1_pts[n - 1] * b.delta_x2[n] for n in b.interval_set) - safety_tol) - - if relaxation_side == RelaxationSide.OVER or relaxation_side == RelaxationSide.BOTH: - b.aux_var_ub1 = pyo.Constraint(expr=(aux_var <= x2_ub * x1 + sum(x1_pts[n - 1] * b.delta_x2[n] for n in b.interval_set) - - (x2_ub - x2_lb) * sum(x1_pts[n - 1] * b.lambda_[n] for n in b.interval_set) + safety_tol)) - b.aux_var_ub2 = pyo.Constraint(expr=aux_var <= x2_lb * x1 + sum(x1_pts[n] * b.delta_x2[n] for n in b.interval_set) + safety_tol) + if ( + relaxation_side == RelaxationSide.UNDER + or relaxation_side == RelaxationSide.BOTH + ): + b.aux_var_lb1 = pyo.Constraint( + expr=( + aux_var + >= x2_ub * x1 + + sum(x1_pts[n] * b.delta_x2[n] for n in b.interval_set) + - (x2_ub - x2_lb) + * sum(x1_pts[n] * b.lambda_[n] for n in b.interval_set) + - safety_tol + ) + ) + b.aux_var_lb2 = pyo.Constraint( + expr=aux_var + >= x2_lb * x1 + + sum(x1_pts[n - 1] * b.delta_x2[n] for n in b.interval_set) + - safety_tol + ) + + if ( + relaxation_side == RelaxationSide.OVER + or relaxation_side == RelaxationSide.BOTH + ): + b.aux_var_ub1 = pyo.Constraint( + expr=( + aux_var + <= x2_ub * x1 + + sum(x1_pts[n - 1] * b.delta_x2[n] for n in b.interval_set) + - (x2_ub - x2_lb) + * sum(x1_pts[n - 1] * b.lambda_[n] for n in b.interval_set) + + safety_tol + ) + ) + b.aux_var_ub2 = pyo.Constraint( + expr=aux_var + <= x2_lb * x1 + + sum(x1_pts[n] * b.delta_x2[n] for n in b.interval_set) + + safety_tol + ) @declare_custom_block(name='PWMcCormickRelaxation') @@ -128,8 +171,15 @@ def vars_with_bounds_in_relaxation(self): return [self._x1, self._x2] def _remove_relaxation(self): - del self._slopes, self._intercepts, self._mccormicks, self._pw, \ - self._mc_index, self._v_index, self._slopes_index + del ( + self._slopes, + self._intercepts, + self._mccormicks, + self._pw, + self._mc_index, + self._v_index, + self._slopes_index, + ) self._mc_index = None self._v_index = None self._slopes_index = None @@ -139,8 +189,16 @@ def _remove_relaxation(self): self._mc_exprs = dict() self._pw = None - def set_input(self, x1, x2, aux_var, relaxation_side=RelaxationSide.BOTH, large_coef=1e5, small_coef=1e-10, - safety_tol=1e-10): + def set_input( + self, + x1, + x2, + aux_var, + relaxation_side=RelaxationSide.BOTH, + large_coef=1e5, + small_coef=1e-10, + safety_tol=1e-10, + ): """ Parameters ---------- @@ -153,18 +211,29 @@ def set_input(self, x1, x2, aux_var, relaxation_side=RelaxationSide.BOTH, large_ relaxation_side : minlp.minlp_defn.RelaxationSide Provide the desired side for the relaxation (OVER, UNDER, or BOTH) """ - super(PWMcCormickRelaxationData, self).set_input(relaxation_side=relaxation_side, - use_linear_relaxation=True, - large_coef=large_coef, small_coef=small_coef, - safety_tol=safety_tol) + super(PWMcCormickRelaxationData, self).set_input( + relaxation_side=relaxation_side, + use_linear_relaxation=True, + large_coef=large_coef, + small_coef=small_coef, + safety_tol=safety_tol, + ) self._x1ref.set_component(x1) self._x2ref.set_component(x2) self._aux_var_ref.set_component(aux_var) self._partitions[self._x1] = _get_bnds_list(self._x1) self._f_x_expr = x1 * x2 - def build(self, x1, x2, aux_var, relaxation_side=RelaxationSide.BOTH, large_coef=1e5, small_coef=1e-10, - safety_tol=1e-10): + def build( + self, + x1, + x2, + aux_var, + relaxation_side=RelaxationSide.BOTH, + large_coef=1e5, + small_coef=1e-10, + safety_tol=1e-10, + ): """ Parameters ---------- @@ -177,8 +246,15 @@ def build(self, x1, x2, aux_var, relaxation_side=RelaxationSide.BOTH, large_coef relaxation_side : minlp.minlp_defn.RelaxationSide Provide the desired side for the relaxation (OVER, UNDER, or BOTH) """ - self.set_input(x1=x1, x2=x2, aux_var=aux_var, relaxation_side=relaxation_side, - large_coef=large_coef, small_coef=small_coef, safety_tol=safety_tol) + self.set_input( + x1=x1, + x2=x2, + aux_var=aux_var, + relaxation_side=relaxation_side, + large_coef=large_coef, + small_coef=small_coef, + safety_tol=safety_tol, + ) self.rebuild() def remove_relaxation(self): @@ -186,8 +262,10 @@ def remove_relaxation(self): self._remove_relaxation() def rebuild(self, build_nonlinear_constraint=False, ensure_oa_at_vertices=True): - super(PWMcCormickRelaxationData, self).rebuild(build_nonlinear_constraint=build_nonlinear_constraint, - ensure_oa_at_vertices=ensure_oa_at_vertices) + super(PWMcCormickRelaxationData, self).rebuild( + build_nonlinear_constraint=build_nonlinear_constraint, + ensure_oa_at_vertices=ensure_oa_at_vertices, + ) if not build_nonlinear_constraint: if self._check_valid_domain_for_relaxation(): if len(self._partitions[self._x1]) == 2: @@ -199,14 +277,27 @@ def rebuild(self, build_nonlinear_constraint=False, ensure_oa_at_vertices=True): self._remove_relaxation() del self._pw self._pw = pe.Block(concrete=True) - _build_pw_mccormick_relaxation(b=self._pw, x1=self._x1, x2=self._x2, aux_var=self._aux_var, - x1_pts=self._partitions[self._x1], - relaxation_side=self.relaxation_side, safety_tol=self.safety_tol) + _build_pw_mccormick_relaxation( + b=self._pw, + x1=self._x1, + x2=self._x2, + aux_var=self._aux_var, + x1_pts=self._partitions[self._x1], + relaxation_side=self.relaxation_side, + safety_tol=self.safety_tol, + ) else: self._remove_relaxation() def _build_mccormicks(self): - del self._mc_index, self._v_index, self._slopes_index, self._slopes, self._intercepts, self._mccormicks + del ( + self._mc_index, + self._v_index, + self._slopes_index, + self._slopes, + self._intercepts, + self._mccormicks, + ) self._mc_exprs = dict() self._mc_index = pe.Set(initialize=[0, 1, 2, 3]) self._v_index = pe.Set(initialize=[1, 2]) @@ -217,17 +308,21 @@ def _build_mccormicks(self): if self.relaxation_side in {RelaxationSide.BOTH, RelaxationSide.UNDER}: for ndx in [0, 1]: - e = LinearExpression(constant=self._intercepts[ndx], - linear_coefs=[self._slopes[ndx, 1], self._slopes[ndx, 2]], - linear_vars=[self._x1, self._x2]) + e = LinearExpression( + constant=self._intercepts[ndx], + linear_coefs=[self._slopes[ndx, 1], self._slopes[ndx, 2]], + linear_vars=[self._x1, self._x2], + ) self._mc_exprs[ndx] = e self._mccormicks[ndx] = self._aux_var >= e if self.relaxation_side in {RelaxationSide.BOTH, RelaxationSide.OVER}: for ndx in [2, 3]: - e = LinearExpression(constant=self._intercepts[ndx], - linear_coefs=[self._slopes[ndx, 1], self._slopes[ndx, 2]], - linear_vars=[self._x1, self._x2]) + e = LinearExpression( + constant=self._intercepts[ndx], + linear_coefs=[self._slopes[ndx, 1], self._slopes[ndx, 2]], + linear_vars=[self._x1, self._x2], + ) self._mc_exprs[ndx] = e self._mccormicks[ndx] = self._aux_var <= e @@ -236,9 +331,13 @@ def _check_expr(self, ndx): rel_side = RelaxationSide.UNDER else: rel_side = RelaxationSide.OVER - success, bad_var, bad_coef, err_msg = _check_cut(self._mc_exprs[ndx], too_small=self.small_coef, - too_large=self.large_coef, relaxation_side=rel_side, - safety_tol=self.safety_tol) + success, bad_var, bad_coef, err_msg = _check_cut( + self._mc_exprs[ndx], + too_small=self.small_coef, + too_large=self.large_coef, + relaxation_side=rel_side, + safety_tol=self.safety_tol, + ) if not success: self._log_bad_cut(bad_var, bad_coef, err_msg) self._mccormicks[ndx].deactivate() diff --git a/pyomo/contrib/coramin/relaxations/multivariate.py b/pyomo/contrib/coramin/relaxations/multivariate.py index 015f173c4ff..ac265da751c 100644 --- a/pyomo/contrib/coramin/relaxations/multivariate.py +++ b/pyomo/contrib/coramin/relaxations/multivariate.py @@ -1,6 +1,9 @@ from pyomo.contrib.coramin.utils.coramin_enums import RelaxationSide, FunctionShape from pyomo.contrib.coramin.relaxations.custom_block import declare_custom_block -from pyomo.contrib.coramin.relaxations.relaxations_base import BaseRelaxationData, ComponentWeakRef +from pyomo.contrib.coramin.relaxations.relaxations_base import ( + BaseRelaxationData, + ComponentWeakRef, +) from pyomo.core.expr.visitor import identify_variables import math import pyomo.environ as pe @@ -29,8 +32,16 @@ def get_rhs_expr(self): def vars_with_bounds_in_relaxation(self): return list() - def set_input(self, aux_var, shape, f_x_expr, use_linear_relaxation=True, large_coef=1e5, small_coef=1e-10, - safety_tol=1e-10): + def set_input( + self, + aux_var, + shape, + f_x_expr, + use_linear_relaxation=True, + large_coef=1e5, + small_coef=1e-10, + safety_tol=1e-10, + ): """ Parameters ---------- @@ -44,24 +55,44 @@ def set_input(self, aux_var, shape, f_x_expr, use_linear_relaxation=True, large_ Specifies whether a linear or nonlinear relaxation should be used """ if shape not in {FunctionShape.CONVEX, FunctionShape.CONCAVE}: - raise ValueError('MultivariateRelaxation only supports concave or convex functions.') + raise ValueError( + 'MultivariateRelaxation only supports concave or convex functions.' + ) self._function_shape = shape if shape == FunctionShape.CONVEX: relaxation_side = RelaxationSide.UNDER else: relaxation_side = RelaxationSide.OVER - super().set_input(relaxation_side=relaxation_side, - use_linear_relaxation=use_linear_relaxation, - large_coef=large_coef, small_coef=small_coef, - safety_tol=safety_tol) + super().set_input( + relaxation_side=relaxation_side, + use_linear_relaxation=use_linear_relaxation, + large_coef=large_coef, + small_coef=small_coef, + safety_tol=safety_tol, + ) self._xs = tuple(identify_variables(f_x_expr, include_fixed=False)) self._aux_var_ref.set_component(aux_var) self._f_x_expr = f_x_expr - def build(self, aux_var, shape, f_x_expr, use_linear_relaxation=True, large_coef=1e5, small_coef=1e-10, - safety_tol=1e-10): - self.set_input(aux_var=aux_var, shape=shape, f_x_expr=f_x_expr, use_linear_relaxation=use_linear_relaxation, - large_coef=large_coef, small_coef=small_coef, safety_tol=safety_tol) + def build( + self, + aux_var, + shape, + f_x_expr, + use_linear_relaxation=True, + large_coef=1e5, + small_coef=1e-10, + safety_tol=1e-10, + ): + self.set_input( + aux_var=aux_var, + shape=shape, + f_x_expr=f_x_expr, + use_linear_relaxation=use_linear_relaxation, + large_coef=large_coef, + small_coef=small_coef, + safety_tol=safety_tol, + ) self.rebuild() def is_rhs_convex(self): @@ -86,8 +117,12 @@ def relaxation_side(self): def relaxation_side(self, val): if self.is_rhs_convex(): if val != RelaxationSide.UNDER: - raise ValueError('MultivariateRelaxations only support underestimators for convex functions') + raise ValueError( + 'MultivariateRelaxations only support underestimators for convex functions' + ) if self.is_rhs_concave(): if val != RelaxationSide.OVER: - raise ValueError('MultivariateRelaxations only support overestimators for concave functions') + raise ValueError( + 'MultivariateRelaxations only support overestimators for concave functions' + ) BaseRelaxationData.relaxation_side.fset(self, val) diff --git a/pyomo/contrib/coramin/relaxations/relaxations_base.py b/pyomo/contrib/coramin/relaxations/relaxations_base.py index eb33f009894..8f1a75077ec 100644 --- a/pyomo/contrib/coramin/relaxations/relaxations_base.py +++ b/pyomo/contrib/coramin/relaxations/relaxations_base.py @@ -16,7 +16,11 @@ from pyomo.core.base.var import _GeneralVarData from pyomo.core.expr.calculus.diff_with_pyomo import reverse_sd from pyomo.core.expr.numeric_expr import LinearExpression, ExpressionBase -from pyomo.core.base.constraint import IndexedConstraint, ScalarConstraint, _GeneralConstraintData +from pyomo.core.base.constraint import ( + IndexedConstraint, + ScalarConstraint, + _GeneralConstraintData, +) from pyomo.contrib.fbbt import interval pyo = pe @@ -28,28 +32,34 @@ class _OACut(object): - def __init__(self, - nonlin_expr, - expr_vars: Sequence[_GeneralVarData], - coefficients: Sequence[_ParamData], - offset: _ParamData): + def __init__( + self, + nonlin_expr, + expr_vars: Sequence[_GeneralVarData], + coefficients: Sequence[_ParamData], + offset: _ParamData, + ): self.expr_vars = expr_vars self.nonlin_expr = nonlin_expr self.coefficients = coefficients self.offset = offset derivs = reverse_sd(self.nonlin_expr) self.derivs = [derivs[i] for i in self.expr_vars] - self.cut_expr = LinearExpression(constant=self.offset, - linear_coefs=self.coefficients, - linear_vars=self.expr_vars) + self.cut_expr = LinearExpression( + constant=self.offset, + linear_coefs=self.coefficients, + linear_vars=self.expr_vars, + ) self.current_pt = None - def update(self, - var_vals: Sequence[float], - relaxation_side: RelaxationSide, - too_small: float, - too_large: float, - safety_tol: float) -> Tuple[bool, Optional[_GeneralVarData], Optional[float], Optional[str]]: + def update( + self, + var_vals: Sequence[float], + relaxation_side: RelaxationSide, + too_small: float, + too_large: float, + safety_tol: float, + ) -> Tuple[bool, Optional[_GeneralVarData], Optional[float], Optional[str]]: res = (True, None, None, None) self.current_pt = var_vals orig_values = [i.value for i in self.expr_vars] @@ -68,8 +78,13 @@ def update(self, for v, val in zip(self.expr_vars, orig_values): v.set_value(val, skip_validation=True) if res[0]: - res = _check_cut(self.cut_expr, too_small=too_small, too_large=too_large, relaxation_side=relaxation_side, - safety_tol=safety_tol) + res = _check_cut( + self.cut_expr, + too_small=too_small, + too_large=too_large, + relaxation_side=relaxation_side, + safety_tol=safety_tol, + ) return res def __repr__(self): @@ -82,7 +97,9 @@ def __str__(self): return self.__repr__() -def _check_cut(cut: LinearExpression, too_small, too_large, relaxation_side, safety_tol): +def _check_cut( + cut: LinearExpression, too_small, too_large, relaxation_side, safety_tol +): res = (True, None, None, None) for coef_p, v in zip(cut.linear_coefs, cut.linear_vars): coef = coef_p.value @@ -91,11 +108,17 @@ def _check_cut(cut: LinearExpression, too_small, too_large, relaxation_side, saf elif 0 < abs(coef) <= too_small and v.has_lb() and v.has_ub(): coef_p._value = 0 if relaxation_side == RelaxationSide.UNDER: - cut.constant._value = interval.add(cut.constant.value, cut.constant.value, - *interval.mul(v.lb, v.ub, coef, coef))[0] + cut.constant._value = interval.add( + cut.constant.value, + cut.constant.value, + *interval.mul(v.lb, v.ub, coef, coef), + )[0] elif relaxation_side == RelaxationSide.OVER: - cut.constant._value = interval.add(cut.constant.value, cut.constant.value, - *interval.mul(v.lb, v.ub, coef, coef))[1] + cut.constant._value = interval.add( + cut.constant.value, + cut.constant.value, + *interval.mul(v.lb, v.ub, coef, coef), + )[1] else: raise ValueError('relaxation_side should be either UNDER or OVER') if relaxation_side == RelaxationSide.UNDER: @@ -130,9 +153,14 @@ def __init__(self, component): self._original_constraint: Optional[ScalarConstraint] = None self._nonlinear: Optional[ScalarConstraint] = None - def set_input(self, relaxation_side=RelaxationSide.BOTH, - use_linear_relaxation=True, large_coef=1e5, - small_coef=1e-10, safety_tol=1e-10): + def set_input( + self, + relaxation_side=RelaxationSide.BOTH, + use_linear_relaxation=True, + large_coef=1e5, + small_coef=1e-10, + safety_tol=1e-10, + ): self.relaxation_side = relaxation_side self.use_linear_relaxation = use_linear_relaxation self._large_coef = large_coef @@ -195,7 +223,11 @@ def use_linear_relaxation(self) -> bool: @use_linear_relaxation.setter def use_linear_relaxation(self, val: bool): if not val: - raise ValueError('Relaxations of type {0} do not support relaxations that are not linear.'.format(type(self))) + raise ValueError( + 'Relaxations of type {0} do not support relaxations that are not linear.'.format( + type(self) + ) + ) def remove_relaxation(self): """ @@ -209,9 +241,15 @@ def remove_relaxation(self): self._nonlinear = None def _has_a_convex_side(self): - if self.has_convex_underestimator() and self.relaxation_side in {RelaxationSide.UNDER, RelaxationSide.BOTH}: + if self.has_convex_underestimator() and self.relaxation_side in { + RelaxationSide.UNDER, + RelaxationSide.BOTH, + }: return True - if self.has_concave_overestimator() and self.relaxation_side in {RelaxationSide.OVER, RelaxationSide.BOTH}: + if self.has_concave_overestimator() and self.relaxation_side in { + RelaxationSide.OVER, + RelaxationSide.BOTH, + }: return True return False @@ -248,11 +286,17 @@ def rebuild(self, build_nonlinear_constraint=False, ensure_oa_at_vertices=True): if build_nonlinear_constraint and self._original_constraint is None: del self._original_constraint if self.relaxation_side == RelaxationSide.BOTH: - self._original_constraint = pe.Constraint(expr=self.get_aux_var() == self.get_rhs_expr()) + self._original_constraint = pe.Constraint( + expr=self.get_aux_var() == self.get_rhs_expr() + ) elif self.relaxation_side == RelaxationSide.UNDER: - self._original_constraint = pe.Constraint(expr=self.get_aux_var() >= self.get_rhs_expr()) + self._original_constraint = pe.Constraint( + expr=self.get_aux_var() >= self.get_rhs_expr() + ) else: - self._original_constraint = pe.Constraint(expr=self.get_aux_var() <= self.get_rhs_expr()) + self._original_constraint = pe.Constraint( + expr=self.get_aux_var() <= self.get_rhs_expr() + ) else: if self._has_a_convex_side(): if self.use_linear_relaxation: @@ -268,10 +312,16 @@ def rebuild(self, build_nonlinear_constraint=False, ensure_oa_at_vertices=True): if self._nonlinear is None: del self._nonlinear if self.has_convex_underestimator(): - self._nonlinear = pe.Constraint(expr=self.get_aux_var() >= self._get_expr_for_oa() - self.safety_tol) + self._nonlinear = pe.Constraint( + expr=self.get_aux_var() + >= self._get_expr_for_oa() - self.safety_tol + ) else: assert self.has_concave_overestimator() - self._nonlinear = pe.Constraint(expr=self.get_aux_var() <= self._get_expr_for_oa() + self.safety_tol) + self._nonlinear = pe.Constraint( + expr=self.get_aux_var() + <= self._get_expr_for_oa() + self.safety_tol + ) def vars_with_bounds_in_relaxation(self): """ @@ -291,7 +341,9 @@ def vars_with_bounds_in_relaxation(self): As another example, take w >= x**2. A linear relaxation of this constraint just involves linear underestimators, which do not depend on the bounds of x or w. Therefore, this method would return an empty list. """ - raise NotImplementedError('This method should be implemented in the derived class.') + raise NotImplementedError( + 'This method should be implemented in the derived class.' + ) def get_deviation(self): """ @@ -323,7 +375,9 @@ def is_rhs_convex(self): ------- bool """ - raise NotImplementedError('This method should be implemented in the derived class.') + raise NotImplementedError( + 'This method should be implemented in the derived class.' + ) def is_rhs_concave(self): """ @@ -334,7 +388,9 @@ def is_rhs_concave(self): ------- bool """ - raise NotImplementedError('This method should be implemented in the derived class.') + raise NotImplementedError( + 'This method should be implemented in the derived class.' + ) def has_convex_underestimator(self): return self.is_rhs_convex() @@ -369,11 +425,14 @@ def pprint(self, ostream=None, verbose=False, prefix=""): if ostream is None: ostream = sys.stdout - ostream.write('{0}{1}: {2}\n'.format(prefix, self.name, self._get_pprint_string())) + ostream.write( + '{0}{1}: {2}\n'.format(prefix, self.name, self._get_pprint_string()) + ) if verbose: - super(BaseRelaxationData, self).pprint(ostream=ostream, - verbose=verbose, prefix=(prefix + ' ')) + super(BaseRelaxationData, self).pprint( + ostream=ostream, verbose=verbose, prefix=(prefix + ' ') + ) def _get_oa_cut(self) -> _OACut: rhs_vars = self.get_rhs_vars() @@ -395,21 +454,31 @@ def _remove_oa_cut(self, oa_cut: _OACut): del self._oa_param_indices[p] del self._oa_params[self._oa_param_indices[oa_cut.offset]] del self._oa_param_indices[oa_cut.offset] - if oa_cut in self._cuts: # if the cut did not pass _check_cut, it won't be in self._cuts + if ( + oa_cut in self._cuts + ): # if the cut did not pass _check_cut, it won't be in self._cuts del self._cuts[oa_cut] def _log_bad_cut(self, fail_var, fail_coef, err_msg): if fail_var is None and fail_coef is None: - logger.debug(f'Encountered exception when adding OA cut ' - f'for "{self._get_pprint_string()}"; Error message: {err_msg}') + logger.debug( + f'Encountered exception when adding OA cut ' + f'for "{self._get_pprint_string()}"; Error message: {err_msg}' + ) elif fail_var is None: - logger.debug(f'Skipped OA cut for "{self._get_pprint_string()}" due to a ' - f'large constant value: {fail_coef}') + logger.debug( + f'Skipped OA cut for "{self._get_pprint_string()}" due to a ' + f'large constant value: {fail_coef}' + ) else: - logger.debug(f'Skipped OA cut for "{self._get_pprint_string()}" due to a ' - f'small or large coefficient for {str(fail_var)}: {fail_coef}') - - def _add_oa_cut(self, pt_tuple: Tuple[float, ...], oa_cut: _OACut) -> Optional[_GeneralConstraintData]: + logger.debug( + f'Skipped OA cut for "{self._get_pprint_string()}" due to a ' + f'small or large coefficient for {str(fail_var)}: {fail_coef}' + ) + + def _add_oa_cut( + self, pt_tuple: Tuple[float, ...], oa_cut: _OACut + ) -> Optional[_GeneralConstraintData]: if self._nonlinear is not None or self._original_constraint is not None: raise ValueError('Can only add an OA cut when using a linear relaxation') if self.has_convex_underestimator(): @@ -417,9 +486,13 @@ def _add_oa_cut(self, pt_tuple: Tuple[float, ...], oa_cut: _OACut) -> Optional[_ else: assert self.has_concave_overestimator() rel_side = RelaxationSide.OVER - cut_info = oa_cut.update(var_vals=pt_tuple, relaxation_side=rel_side, - too_small=self.small_coef, too_large=self.large_coef, - safety_tol=self.safety_tol) + cut_info = oa_cut.update( + var_vals=pt_tuple, + relaxation_side=rel_side, + too_small=self.small_coef, + too_large=self.large_coef, + safety_tol=self.safety_tol, + ) success, fail_var, fail_coef, err_msg = cut_info if not success: self._log_bad_cut(fail_var, fail_coef, err_msg) @@ -448,7 +521,12 @@ def _add_oa_point(self, pt_tuple: Tuple[float, ...]): if pt_tuple not in self._oa_points: self._oa_points[pt_tuple] = self._get_oa_cut() - def add_oa_point(self, var_values: Optional[Union[Tuple[float, ...], Mapping[_GeneralVarData, float]]] = None): + def add_oa_point( + self, + var_values: Optional[ + Union[Tuple[float, ...], Mapping[_GeneralVarData, float]] + ] = None, + ): """ Add a point at which an outer-approximation cut for a convex constraint should be added. This does not rebuild the relaxation. You must call rebuild() for the constraint to get added. @@ -502,7 +580,9 @@ def pop_oa_points(self, key=None): for pt_tuple in list_of_points: self._add_oa_point(pt_tuple) - def add_cut(self, keep_cut=True, check_violation=True, feasibility_tol=1e-8) -> Optional[_GeneralConstraintData]: + def add_cut( + self, keep_cut=True, check_violation=True, feasibility_tol=1e-8 + ) -> Optional[_GeneralConstraintData]: """ This function will add a linear cut to the relaxation. Cuts are only generated for the convex side of the constraint (if the constraint has a convex side). For example, if the relaxation is a PWXSquaredRelaxationData @@ -610,7 +690,7 @@ def clean_oa_points(self, ensure_oa_at_vertices=True): if ub_tuple not in self._oa_points: if len(self._oa_points) <= 1: self._add_oa_point(ub_tuple) - else: # move the largest point to ub_tuple + else: # move the largest point to ub_tuple max_pt = max(self._oa_points.keys()) max_oa_cut = self._oa_points[max_pt] del self._oa_points[max_pt] @@ -629,15 +709,27 @@ def rebuild(self, build_nonlinear_constraint=False, ensure_oa_at_vertices=True): """ Remove any auto-created vars/constraints from the relaxation block and recreate it """ - super(BasePWRelaxationData, self).rebuild(build_nonlinear_constraint=build_nonlinear_constraint, - ensure_oa_at_vertices=ensure_oa_at_vertices) + super(BasePWRelaxationData, self).rebuild( + build_nonlinear_constraint=build_nonlinear_constraint, + ensure_oa_at_vertices=ensure_oa_at_vertices, + ) self.clean_partitions() - def set_input(self, relaxation_side=RelaxationSide.BOTH, use_linear_relaxation=True, large_coef=1e5, - small_coef=1e-10, safety_tol=1e-10): - super(BasePWRelaxationData, self).set_input(relaxation_side=relaxation_side, - use_linear_relaxation=use_linear_relaxation, large_coef=large_coef, - small_coef=small_coef, safety_tol=safety_tol) + def set_input( + self, + relaxation_side=RelaxationSide.BOTH, + use_linear_relaxation=True, + large_coef=1e5, + small_coef=1e-10, + safety_tol=1e-10, + ): + super(BasePWRelaxationData, self).set_input( + relaxation_side=relaxation_side, + use_linear_relaxation=use_linear_relaxation, + large_coef=large_coef, + small_coef=small_coef, + safety_tol=safety_tol, + ) self._partitions = ComponentMap() self._saved_partitions = list() @@ -646,7 +738,9 @@ def add_partition_point(self): Add a point to the current partitioning. This does not rebuild the relaxation. You must call rebuild() to rebuild the relaxation. """ - raise NotImplementedError('This method should be implemented in the derived class.') + raise NotImplementedError( + 'This method should be implemented in the derived class.' + ) def _add_partition_point(self, var, value=None): if value is None: @@ -659,7 +753,9 @@ def push_partitions(self): """ Save the current partitioning for later use through pop_partitions(). """ - self._saved_partitions.append(pe.ComponentMap((k, list(v)) for k, v in self._partitions.items())) + self._saved_partitions.append( + pe.ComponentMap((k, list(v)) for k, v in self._partitions.items()) + ) def clear_partitions(self): """ @@ -701,7 +797,9 @@ def get_active_partitions(self): lower = None upper = None if not (pts[0] - 1e-6 <= val <= pts[-1] + 1e-6): - raise ValueError('The variable value must be within the variable bounds') + raise ValueError( + 'The variable value must be within the variable bounds' + ) if val < pts[0]: lower = pts[0] upper = pts[1] @@ -724,6 +822,7 @@ class ComponentWeakRef(object): """ This object is used to reference components from a block that are not owned by that block. """ + # ToDo: Example in the documentation def __init__(self, comp): self.compref = None diff --git a/pyomo/contrib/coramin/relaxations/tests/test_alphabb.py b/pyomo/contrib/coramin/relaxations/tests/test_alphabb.py index ef9b98f8943..28b5c067866 100644 --- a/pyomo/contrib/coramin/relaxations/tests/test_alphabb.py +++ b/pyomo/contrib/coramin/relaxations/tests/test_alphabb.py @@ -15,14 +15,15 @@ def setUpClass(cls): model.y = pe.Var(bounds=(-1, 1)) model.w = pe.Var() - model.f_x = pe.cos(model.x)*pe.sin(model.y) - model.x/(model.y**2 + 1) + model.f_x = pe.cos(model.x) * pe.sin(model.y) - model.x / (model.y**2 + 1) model.obj = pe.Objective(expr=model.w) model.abb = AlphaBBRelaxation() model.abb.build( - aux_var=model.w, f_x_expr=model.f_x, + aux_var=model.w, + f_x_expr=model.f_x, relaxation_side=coramin.RelaxationSide.UNDER, - eigenvalue_bounder=coramin.EigenValueBounder.GershgorinWithSimplification + eigenvalue_bounder=coramin.EigenValueBounder.GershgorinWithSimplification, ) def test_nonlinear(self): diff --git a/pyomo/contrib/coramin/relaxations/tests/test_auto_relax.py b/pyomo/contrib/coramin/relaxations/tests/test_auto_relax.py index 70aecf12707..49bf9f28ec1 100644 --- a/pyomo/contrib/coramin/relaxations/tests/test_auto_relax.py +++ b/pyomo/contrib/coramin/relaxations/tests/test_auto_relax.py @@ -15,10 +15,10 @@ class TestAutoRelax(unittest.TestCase): def test_product1(self): m = pe.ConcreteModel() - m.x = pe.Var(bounds=(-1,1)) - m.y = pe.Var(bounds=(-1,1)) + m.x = pe.Var(bounds=(-1, 1)) + m.y = pe.Var(bounds=(-1, 1)) m.z = pe.Var() - m.c = pe.Constraint(expr=m.z - m.x*m.y == 0) + m.c = pe.Constraint(expr=m.z - m.x * m.y == 0) rel = coramin.relaxations.relax(m, use_alpha_bb=False) @@ -37,19 +37,21 @@ def test_product1(self): self.assertTrue(hasattr(rel, 'relaxations')) self.assertTrue(hasattr(rel.relaxations, 'rel0')) - self.assertTrue(isinstance(rel.relaxations.rel0, coramin.relaxations.PWMcCormickRelaxation)) + self.assertTrue( + isinstance(rel.relaxations.rel0, coramin.relaxations.PWMcCormickRelaxation) + ) self.assertIn(rel.x, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) self.assertIn(rel.y, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) self.assertEqual(id(rel.aux_vars[1]), id(rel.relaxations.rel0.get_aux_var())) def test_product2(self): m = pe.ConcreteModel() - m.x = pe.Var(bounds=(-1,1)) - m.y = pe.Var(bounds=(-1,1)) + m.x = pe.Var(bounds=(-1, 1)) + m.y = pe.Var(bounds=(-1, 1)) m.z = pe.Var() m.v = pe.Var() - m.c1 = pe.Constraint(expr=m.z - m.x*m.y == 0) - m.c2 = pe.Constraint(expr=m.v - 3*m.x*m.y == 0) + m.c1 = pe.Constraint(expr=m.z - m.x * m.y == 0) + m.c2 = pe.Constraint(expr=m.v - 3 * m.x * m.y == 0) rel = coramin.relaxations.relax(m, use_alpha_bb=False) @@ -76,17 +78,19 @@ def test_product2(self): self.assertTrue(hasattr(rel, 'relaxations')) self.assertTrue(hasattr(rel.relaxations, 'rel0')) - self.assertTrue(isinstance(rel.relaxations.rel0, coramin.relaxations.PWMcCormickRelaxation)) + self.assertTrue( + isinstance(rel.relaxations.rel0, coramin.relaxations.PWMcCormickRelaxation) + ) self.assertIn(rel.x, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) self.assertIn(rel.y, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) self.assertEqual(id(rel.aux_vars[1]), id(rel.relaxations.rel0.get_aux_var())) def test_product3(self): m = pe.ConcreteModel() - m.x = pe.Var(bounds=(-1,1)) - m.y = pe.Var(bounds=(-1,1)) + m.x = pe.Var(bounds=(-1, 1)) + m.y = pe.Var(bounds=(-1, 1)) m.z = pe.Var() - m.c = pe.Constraint(expr=m.z - m.x*m.y*3 == 0) + m.c = pe.Constraint(expr=m.z - m.x * m.y * 3 == 0) rel = coramin.relaxations.relax(m, use_alpha_bb=False) @@ -105,7 +109,9 @@ def test_product3(self): self.assertTrue(hasattr(rel, 'relaxations')) self.assertTrue(hasattr(rel.relaxations, 'rel0')) - self.assertTrue(isinstance(rel.relaxations.rel0, coramin.relaxations.PWMcCormickRelaxation)) + self.assertTrue( + isinstance(rel.relaxations.rel0, coramin.relaxations.PWMcCormickRelaxation) + ) self.assertIn(rel.x, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) self.assertIn(rel.y, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) self.assertEqual(id(rel.aux_vars[1]), id(rel.relaxations.rel0.get_aux_var())) @@ -114,9 +120,9 @@ def test_product3(self): def test_product4(self): m = pe.ConcreteModel() - m.x = pe.Var(bounds=(-1,1)) + m.x = pe.Var(bounds=(-1, 1)) m.z = pe.Var() - m.c = pe.Constraint(expr=m.z - m.x*m.x == 0) + m.c = pe.Constraint(expr=m.z - m.x * m.x == 0) rel = coramin.relaxations.relax(m, use_alpha_bb=False) @@ -135,7 +141,11 @@ def test_product4(self): self.assertTrue(hasattr(rel, 'relaxations')) self.assertTrue(hasattr(rel.relaxations, 'rel0')) - self.assertTrue(isinstance(rel.relaxations.rel0, coramin.relaxations.PWXSquaredRelaxationData)) + self.assertTrue( + isinstance( + rel.relaxations.rel0, coramin.relaxations.PWXSquaredRelaxationData + ) + ) self.assertIn(rel.x, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) self.assertEqual(len(rel.relaxations.rel0.get_rhs_vars()), 1) self.assertEqual(id(rel.aux_vars[1]), id(rel.relaxations.rel0.get_aux_var())) @@ -144,12 +154,12 @@ def test_product4(self): def test_quadratic(self): m = pe.ConcreteModel() - m.x = pe.Var(bounds=(-1,1)) + m.x = pe.Var(bounds=(-1, 1)) m.y = pe.Var() m.z = pe.Var() m.w = pe.Var() m.c = pe.Constraint(expr=m.x**2 + m.y + m.z == 0) - m.c2 = pe.Constraint(expr=m.w - 3*m.x**2 == 0) + m.c2 = pe.Constraint(expr=m.w - 3 * m.x**2 == 0) rel = coramin.relaxations.relax(m, use_alpha_bb=False) @@ -177,19 +187,21 @@ def test_quadratic(self): self.assertTrue(hasattr(rel, 'relaxations')) self.assertTrue(hasattr(rel.relaxations, 'rel0')) - self.assertTrue(isinstance(rel.relaxations.rel0, coramin.relaxations.PWXSquaredRelaxation)) + self.assertTrue( + isinstance(rel.relaxations.rel0, coramin.relaxations.PWXSquaredRelaxation) + ) self.assertIn(rel.x, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) self.assertEqual(id(rel.aux_vars[1]), id(rel.relaxations.rel0.get_aux_var())) self.assertFalse(hasattr(rel.relaxations, 'rel1')) def test_cubic_convex(self): m = pe.ConcreteModel() - m.x = pe.Var(bounds=(1,2)) + m.x = pe.Var(bounds=(1, 2)) m.y = pe.Var() m.z = pe.Var() m.w = pe.Var() m.c = pe.Constraint(expr=m.x**3 + m.y + m.z == 0) - m.c2 = pe.Constraint(expr=m.w - 3*m.x**3 == 0) + m.c2 = pe.Constraint(expr=m.w - 3 * m.x**3 == 0) rel = coramin.relaxations.relax(m, use_alpha_bb=False) @@ -217,7 +229,9 @@ def test_cubic_convex(self): self.assertTrue(hasattr(rel, 'relaxations')) self.assertTrue(hasattr(rel.relaxations, 'rel0')) - self.assertTrue(isinstance(rel.relaxations.rel0, coramin.relaxations.PWUnivariateRelaxation)) + self.assertTrue( + isinstance(rel.relaxations.rel0, coramin.relaxations.PWUnivariateRelaxation) + ) self.assertIn(rel.x, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) self.assertEqual(id(rel.aux_vars[1]), id(rel.relaxations.rel0.get_aux_var())) self.assertTrue(rel.relaxations.rel0.is_rhs_convex()) @@ -226,12 +240,12 @@ def test_cubic_convex(self): def test_cubic_concave(self): m = pe.ConcreteModel() - m.x = pe.Var(bounds=(-2,-1)) + m.x = pe.Var(bounds=(-2, -1)) m.y = pe.Var() m.z = pe.Var() m.w = pe.Var() m.c = pe.Constraint(expr=m.x**3 + m.y + m.z == 0) - m.c2 = pe.Constraint(expr=m.w - 3*m.x**3 == 0) + m.c2 = pe.Constraint(expr=m.w - 3 * m.x**3 == 0) rel = coramin.relaxations.relax(m, use_alpha_bb=False) @@ -259,7 +273,9 @@ def test_cubic_concave(self): self.assertTrue(hasattr(rel, 'relaxations')) self.assertTrue(hasattr(rel.relaxations, 'rel0')) - self.assertTrue(isinstance(rel.relaxations.rel0, coramin.relaxations.PWUnivariateRelaxation)) + self.assertTrue( + isinstance(rel.relaxations.rel0, coramin.relaxations.PWUnivariateRelaxation) + ) self.assertIn(rel.x, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) self.assertEqual(id(rel.aux_vars[1]), id(rel.relaxations.rel0.get_aux_var())) self.assertFalse(rel.relaxations.rel0.is_rhs_convex()) @@ -268,12 +284,12 @@ def test_cubic_concave(self): def test_cubic(self): m = pe.ConcreteModel() - m.x = pe.Var(bounds=(-1,1)) + m.x = pe.Var(bounds=(-1, 1)) m.y = pe.Var() m.z = pe.Var() m.w = pe.Var() m.c = pe.Constraint(expr=m.x**3 + m.y + m.z == 0) - m.c2 = pe.Constraint(expr=m.w - 3*m.x**3 == 0) + m.c2 = pe.Constraint(expr=m.w - 3 * m.x**3 == 0) rel = coramin.relaxations.relax(m, use_alpha_bb=False) @@ -312,25 +328,31 @@ def test_cubic(self): self.assertTrue(hasattr(rel, 'relaxations')) self.assertTrue(hasattr(rel.relaxations, 'rel0')) - self.assertTrue(isinstance(rel.relaxations.rel0, coramin.relaxations.PWXSquaredRelaxation)) + self.assertTrue( + isinstance(rel.relaxations.rel0, coramin.relaxations.PWXSquaredRelaxation) + ) self.assertIn(rel.x, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) self.assertEqual(id(rel.aux_vars[1]), id(rel.relaxations.rel0.get_aux_var())) self.assertTrue(hasattr(rel.relaxations, 'rel1')) - self.assertTrue(isinstance(rel.relaxations.rel1, coramin.relaxations.PWMcCormickRelaxation)) + self.assertTrue( + isinstance(rel.relaxations.rel1, coramin.relaxations.PWMcCormickRelaxation) + ) self.assertIn(rel.x, ComponentSet(rel.relaxations.rel1.get_rhs_vars())) - self.assertIn(rel.aux_vars[1], ComponentSet(rel.relaxations.rel1.get_rhs_vars())) + self.assertIn( + rel.aux_vars[1], ComponentSet(rel.relaxations.rel1.get_rhs_vars()) + ) self.assertEqual(id(rel.aux_vars[2]), id(rel.relaxations.rel1.get_aux_var())) def test_pow_fractional1(self): m = pe.ConcreteModel() - m.x = pe.Var(bounds=(-1,1)) + m.x = pe.Var(bounds=(-1, 1)) m.y = pe.Var() m.z = pe.Var() m.w = pe.Var() m.p = pe.Param(initialize=0.5) m.c = pe.Constraint(expr=m.x**m.p + m.y + m.z == 0) - m.c2 = pe.Constraint(expr=m.w - 3*m.x**m.p == 0) + m.c2 = pe.Constraint(expr=m.w - 3 * m.x**m.p == 0) rel = coramin.relaxations.relax(m, use_alpha_bb=False) @@ -358,7 +380,9 @@ def test_pow_fractional1(self): self.assertTrue(hasattr(rel, 'relaxations')) self.assertTrue(hasattr(rel.relaxations, 'rel0')) - self.assertTrue(isinstance(rel.relaxations.rel0, coramin.relaxations.PWUnivariateRelaxation)) + self.assertTrue( + isinstance(rel.relaxations.rel0, coramin.relaxations.PWUnivariateRelaxation) + ) self.assertIn(rel.x, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) self.assertEqual(id(rel.aux_vars[1]), id(rel.relaxations.rel0.get_aux_var())) self.assertFalse(rel.relaxations.rel0.is_rhs_convex()) @@ -367,13 +391,13 @@ def test_pow_fractional1(self): def test_pow_fractional2(self): m = pe.ConcreteModel() - m.x = pe.Var(bounds=(-1,1)) + m.x = pe.Var(bounds=(-1, 1)) m.y = pe.Var() m.z = pe.Var() m.w = pe.Var() m.p = pe.Param(initialize=1.5) m.c = pe.Constraint(expr=m.x**m.p + m.y + m.z == 0) - m.c2 = pe.Constraint(expr=m.w - 3*m.x**m.p == 0) + m.c2 = pe.Constraint(expr=m.w - 3 * m.x**m.p == 0) rel = coramin.relaxations.relax(m, use_alpha_bb=False) @@ -401,7 +425,9 @@ def test_pow_fractional2(self): self.assertTrue(hasattr(rel, 'relaxations')) self.assertTrue(hasattr(rel.relaxations, 'rel0')) - self.assertTrue(isinstance(rel.relaxations.rel0, coramin.relaxations.PWUnivariateRelaxation)) + self.assertTrue( + isinstance(rel.relaxations.rel0, coramin.relaxations.PWUnivariateRelaxation) + ) self.assertIn(rel.x, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) self.assertEqual(id(rel.aux_vars[1]), id(rel.relaxations.rel0.get_aux_var())) self.assertTrue(rel.relaxations.rel0.is_rhs_convex()) @@ -410,13 +436,13 @@ def test_pow_fractional2(self): def test_pow_neg_even1(self): m = pe.ConcreteModel() - m.x = pe.Var(bounds=(1,2)) + m.x = pe.Var(bounds=(1, 2)) m.y = pe.Var() m.z = pe.Var() m.w = pe.Var() m.p = pe.Param(initialize=-2) m.c = pe.Constraint(expr=m.x**m.p + m.y + m.z == 0) - m.c2 = pe.Constraint(expr=m.w - 3*m.x**m.p == 0) + m.c2 = pe.Constraint(expr=m.w - 3 * m.x**m.p == 0) rel = coramin.relaxations.relax(m, use_alpha_bb=False) @@ -444,7 +470,9 @@ def test_pow_neg_even1(self): self.assertTrue(hasattr(rel, 'relaxations')) self.assertTrue(hasattr(rel.relaxations, 'rel0')) - self.assertTrue(isinstance(rel.relaxations.rel0, coramin.relaxations.PWUnivariateRelaxation)) + self.assertTrue( + isinstance(rel.relaxations.rel0, coramin.relaxations.PWUnivariateRelaxation) + ) self.assertIn(rel.x, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) self.assertEqual(id(rel.aux_vars[1]), id(rel.relaxations.rel0.get_aux_var())) self.assertTrue(rel.relaxations.rel0.is_rhs_convex()) @@ -453,13 +481,13 @@ def test_pow_neg_even1(self): def test_pow_neg_even2(self): m = pe.ConcreteModel() - m.x = pe.Var(bounds=(-2,-1)) + m.x = pe.Var(bounds=(-2, -1)) m.y = pe.Var() m.z = pe.Var() m.w = pe.Var() m.p = pe.Param(initialize=-2) m.c = pe.Constraint(expr=m.x**m.p + m.y + m.z == 0) - m.c2 = pe.Constraint(expr=m.w - 3*m.x**m.p == 0) + m.c2 = pe.Constraint(expr=m.w - 3 * m.x**m.p == 0) rel = coramin.relaxations.relax(m, use_alpha_bb=False) @@ -487,7 +515,9 @@ def test_pow_neg_even2(self): self.assertTrue(hasattr(rel, 'relaxations')) self.assertTrue(hasattr(rel.relaxations, 'rel0')) - self.assertTrue(isinstance(rel.relaxations.rel0, coramin.relaxations.PWUnivariateRelaxation)) + self.assertTrue( + isinstance(rel.relaxations.rel0, coramin.relaxations.PWUnivariateRelaxation) + ) self.assertIn(rel.x, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) self.assertEqual(id(rel.aux_vars[1]), id(rel.relaxations.rel0.get_aux_var())) self.assertTrue(rel.relaxations.rel0.is_rhs_convex()) @@ -496,13 +526,13 @@ def test_pow_neg_even2(self): def test_pow_neg_odd1(self): m = pe.ConcreteModel() - m.x = pe.Var(bounds=(1,2)) + m.x = pe.Var(bounds=(1, 2)) m.y = pe.Var() m.z = pe.Var() m.w = pe.Var() m.p = pe.Param(initialize=-3) m.c = pe.Constraint(expr=m.x**m.p + m.y + m.z == 0) - m.c2 = pe.Constraint(expr=m.w - 3*m.x**m.p == 0) + m.c2 = pe.Constraint(expr=m.w - 3 * m.x**m.p == 0) rel = coramin.relaxations.relax(m, use_alpha_bb=False) @@ -530,7 +560,9 @@ def test_pow_neg_odd1(self): self.assertTrue(hasattr(rel, 'relaxations')) self.assertTrue(hasattr(rel.relaxations, 'rel0')) - self.assertTrue(isinstance(rel.relaxations.rel0, coramin.relaxations.PWUnivariateRelaxation)) + self.assertTrue( + isinstance(rel.relaxations.rel0, coramin.relaxations.PWUnivariateRelaxation) + ) self.assertIn(rel.x, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) self.assertEqual(id(rel.aux_vars[1]), id(rel.relaxations.rel0.get_aux_var())) self.assertTrue(rel.relaxations.rel0.is_rhs_convex()) @@ -539,13 +571,13 @@ def test_pow_neg_odd1(self): def test_pow_neg_odd2(self): m = pe.ConcreteModel() - m.x = pe.Var(bounds=(-2,-1)) + m.x = pe.Var(bounds=(-2, -1)) m.y = pe.Var() m.z = pe.Var() m.w = pe.Var() m.p = pe.Param(initialize=-3) m.c = pe.Constraint(expr=m.x**m.p + m.y + m.z == 0) - m.c2 = pe.Constraint(expr=m.w - 3*m.x**m.p == 0) + m.c2 = pe.Constraint(expr=m.w - 3 * m.x**m.p == 0) rel = coramin.relaxations.relax(m, use_alpha_bb=False) @@ -573,7 +605,9 @@ def test_pow_neg_odd2(self): self.assertTrue(hasattr(rel, 'relaxations')) self.assertTrue(hasattr(rel.relaxations, 'rel0')) - self.assertTrue(isinstance(rel.relaxations.rel0, coramin.relaxations.PWUnivariateRelaxation)) + self.assertTrue( + isinstance(rel.relaxations.rel0, coramin.relaxations.PWUnivariateRelaxation) + ) self.assertIn(rel.x, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) self.assertEqual(id(rel.aux_vars[1]), id(rel.relaxations.rel0.get_aux_var())) self.assertFalse(rel.relaxations.rel0.is_rhs_convex()) @@ -582,13 +616,13 @@ def test_pow_neg_odd2(self): def test_pow_neg(self): m = pe.ConcreteModel() - m.x = pe.Var(bounds=(-1,1)) + m.x = pe.Var(bounds=(-1, 1)) m.y = pe.Var() m.z = pe.Var() m.w = pe.Var() m.p = pe.Param(initialize=-2) m.c = pe.Constraint(expr=m.x**m.p + m.y + m.z == 0) - m.c2 = pe.Constraint(expr=m.w - 3*m.x**m.p == 0) + m.c2 = pe.Constraint(expr=m.w - 3 * m.x**m.p == 0) rel = coramin.relaxations.relax(m, use_alpha_bb=False) @@ -629,16 +663,24 @@ def test_pow_neg(self): self.assertTrue(hasattr(rel, 'relaxations')) self.assertTrue(hasattr(rel.relaxations, 'rel0')) - self.assertTrue(isinstance(rel.relaxations.rel0, coramin.relaxations.PWXSquaredRelaxation)) + self.assertTrue( + isinstance(rel.relaxations.rel0, coramin.relaxations.PWXSquaredRelaxation) + ) self.assertIn(rel.x, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) self.assertEqual(id(rel.aux_vars[1]), id(rel.relaxations.rel0.get_aux_var())) self.assertTrue(rel.relaxations.rel0.is_rhs_convex()) self.assertFalse(rel.relaxations.rel0.is_rhs_concave()) self.assertTrue(hasattr(rel.relaxations, 'rel1')) - self.assertTrue(isinstance(rel.relaxations.rel1, coramin.relaxations.PWMcCormickRelaxation)) - self.assertIn(rel.aux_vars[1], ComponentSet(rel.relaxations.rel1.get_rhs_vars())) - self.assertIn(rel.aux_vars[2], ComponentSet(rel.relaxations.rel1.get_rhs_vars())) + self.assertTrue( + isinstance(rel.relaxations.rel1, coramin.relaxations.PWMcCormickRelaxation) + ) + self.assertIn( + rel.aux_vars[1], ComponentSet(rel.relaxations.rel1.get_rhs_vars()) + ) + self.assertIn( + rel.aux_vars[2], ComponentSet(rel.relaxations.rel1.get_rhs_vars()) + ) self.assertEqual(id(rel.aux_vars[3]), id(rel.relaxations.rel1.get_aux_var())) self.assertFalse(hasattr(rel.relaxations, 'rel2')) @@ -647,16 +689,29 @@ def test_sqrt(self): m = pe.ConcreteModel() m.x = pe.Var() m.z = pe.Var() - m.c = pe.Constraint(expr=m.z + pe.sqrt(2*pe.log(m.x)) <= 1) + m.c = pe.Constraint(expr=m.z + pe.sqrt(2 * pe.log(m.x)) <= 1) coramin.relaxations.relax(m, in_place=True, use_fbbt=False, use_alpha_bb=False) - rels = list(coramin.relaxations.relaxation_data_objects(m, descend_into=True, active=True, sort=True)) + rels = list( + coramin.relaxations.relaxation_data_objects( + m, descend_into=True, active=True, sort=True + ) + ) self.assertEqual(len(rels), 2) rel0 = m.relaxations.rel0 # log rel1 = m.relaxations.rel1 # sqrt self.assertEqual(sympyify_expression(rel0.get_rhs_expr() - pe.log(m.x))[1], 0) - self.assertEqual(sympyify_expression(rel1.get_rhs_expr() - m.aux_vars[3]**0.5)[1], 0) - self.assertEqual(sympyify_expression(m.aux_cons[1].body - m.aux_vars[3] + 2 * m.aux_vars[1])[1], 0) - self.assertEqual(sympyify_expression(m.aux_cons[2].body - m.z - m.aux_vars[2])[1], 0) + self.assertEqual( + sympyify_expression(rel1.get_rhs_expr() - m.aux_vars[3] ** 0.5)[1], 0 + ) + self.assertEqual( + sympyify_expression(m.aux_cons[1].body - m.aux_vars[3] + 2 * m.aux_vars[1])[ + 1 + ], + 0, + ) + self.assertEqual( + sympyify_expression(m.aux_cons[2].body - m.z - m.aux_vars[2])[1], 0 + ) self.assertEqual(m.aux_cons[1].lower, 0) self.assertEqual(m.aux_cons[2].lower, None) self.assertEqual(m.aux_cons[2].upper, 1) @@ -667,12 +722,12 @@ def test_sqrt(self): def test_exp(self): m = pe.ConcreteModel() - m.x = pe.Var(bounds=(-1,1)) - m.y = pe.Var(bounds=(-1,1)) + m.x = pe.Var(bounds=(-1, 1)) + m.y = pe.Var(bounds=(-1, 1)) m.z = pe.Var() m.w = pe.Var() - m.c = pe.Constraint(expr=pe.exp(m.x*m.y) + m.z == 0) - m.c2 = pe.Constraint(expr=m.w - 3*pe.exp(m.x*m.y) == 0) + m.c = pe.Constraint(expr=pe.exp(m.x * m.y) + m.z == 0) + m.c2 = pe.Constraint(expr=m.w - 3 * pe.exp(m.x * m.y) == 0) rel = coramin.relaxations.relax(m, use_alpha_bb=False) @@ -703,13 +758,17 @@ def test_exp(self): self.assertTrue(hasattr(rel, 'relaxations')) self.assertTrue(hasattr(rel.relaxations, 'rel0')) - self.assertTrue(isinstance(rel.relaxations.rel0, coramin.relaxations.PWMcCormickRelaxation)) + self.assertTrue( + isinstance(rel.relaxations.rel0, coramin.relaxations.PWMcCormickRelaxation) + ) self.assertIn(rel.x, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) self.assertIn(rel.y, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) self.assertEqual(id(rel.aux_vars[1]), id(rel.relaxations.rel0.get_aux_var())) self.assertTrue(hasattr(rel.relaxations, 'rel1')) - self.assertTrue(isinstance(rel.relaxations.rel1, coramin.relaxations.PWUnivariateRelaxation)) + self.assertTrue( + isinstance(rel.relaxations.rel1, coramin.relaxations.PWUnivariateRelaxation) + ) self.assertEqual(id(rel.aux_vars[1]), id(rel.relaxations.rel1._x)) self.assertEqual(id(rel.aux_vars[2]), id(rel.relaxations.rel1.get_aux_var())) self.assertTrue(rel.relaxations.rel1.is_rhs_convex()) @@ -719,12 +778,12 @@ def test_exp(self): def test_log(self): m = pe.ConcreteModel() - m.x = pe.Var(bounds=(1,2)) - m.y = pe.Var(bounds=(1,2)) + m.x = pe.Var(bounds=(1, 2)) + m.y = pe.Var(bounds=(1, 2)) m.z = pe.Var() m.w = pe.Var() - m.c = pe.Constraint(expr=pe.log(m.x*m.y) + m.z == 0) - m.c2 = pe.Constraint(expr=m.w - 3*pe.log(m.x*m.y) == 0) + m.c = pe.Constraint(expr=pe.log(m.x * m.y) + m.z == 0) + m.c2 = pe.Constraint(expr=m.w - 3 * pe.log(m.x * m.y) == 0) rel = coramin.relaxations.relax(m, use_alpha_bb=False) @@ -755,14 +814,20 @@ def test_log(self): self.assertTrue(hasattr(rel, 'relaxations')) self.assertTrue(hasattr(rel.relaxations, 'rel0')) - self.assertTrue(isinstance(rel.relaxations.rel0, coramin.relaxations.PWMcCormickRelaxation)) + self.assertTrue( + isinstance(rel.relaxations.rel0, coramin.relaxations.PWMcCormickRelaxation) + ) self.assertIn(rel.x, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) self.assertIn(rel.y, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) self.assertEqual(id(rel.aux_vars[1]), id(rel.relaxations.rel0.get_aux_var())) self.assertTrue(hasattr(rel.relaxations, 'rel1')) - self.assertTrue(isinstance(rel.relaxations.rel1, coramin.relaxations.PWUnivariateRelaxation)) - self.assertIn(rel.aux_vars[1], ComponentSet(rel.relaxations.rel1.get_rhs_vars())) + self.assertTrue( + isinstance(rel.relaxations.rel1, coramin.relaxations.PWUnivariateRelaxation) + ) + self.assertIn( + rel.aux_vars[1], ComponentSet(rel.relaxations.rel1.get_rhs_vars()) + ) self.assertEqual(id(rel.aux_vars[2]), id(rel.relaxations.rel1.get_aux_var())) self.assertFalse(rel.relaxations.rel1.is_rhs_convex()) self.assertTrue(rel.relaxations.rel1.is_rhs_concave()) @@ -774,12 +839,18 @@ def test_div1(self): m.x = pe.Var(bounds=(-1, 1)) m.y = pe.Var(bounds=(1, 2)) m.z = pe.Var() - m.c = pe.Constraint(expr=m.z - m.x/m.y == 0) + m.c = pe.Constraint(expr=m.z - m.x / m.y == 0) rel = coramin.relaxations.relax(m, in_place=True, use_alpha_bb=False) self.assertIs(m, rel) relaxations = list(coramin.relaxations.relaxation_data_objects(m)) - constraints = list(coramin.relaxations.nonrelaxation_component_data_objects(m, ctype=pe.Constraint)) - vars = list(coramin.relaxations.nonrelaxation_component_data_objects(m, ctype=pe.Var)) + constraints = list( + coramin.relaxations.nonrelaxation_component_data_objects( + m, ctype=pe.Constraint + ) + ) + vars = list( + coramin.relaxations.nonrelaxation_component_data_objects(m, ctype=pe.Var) + ) self.assertEqual(len(relaxations), 1) self.assertEqual(len(constraints), 1) self.assertEqual(len(vars), 4) @@ -798,10 +869,10 @@ def test_div1(self): def test_div2(self): m = pe.ConcreteModel() - m.x = pe.Var(bounds=(-1,1)) - m.y = pe.Var(bounds=(-1,1)) + m.x = pe.Var(bounds=(-1, 1)) + m.y = pe.Var(bounds=(-1, 1)) m.z = pe.Var() - m.c = pe.Constraint(expr=m.z - m.x*m.y/2 == 0) + m.c = pe.Constraint(expr=m.z - m.x * m.y / 2 == 0) rel = coramin.relaxations.relax(m, use_alpha_bb=False) @@ -820,7 +891,9 @@ def test_div2(self): self.assertTrue(hasattr(rel, 'relaxations')) self.assertTrue(hasattr(rel.relaxations, 'rel0')) - self.assertTrue(isinstance(rel.relaxations.rel0, coramin.relaxations.PWMcCormickRelaxation)) + self.assertTrue( + isinstance(rel.relaxations.rel0, coramin.relaxations.PWMcCormickRelaxation) + ) self.assertIn(rel.x, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) self.assertIn(rel.y, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) self.assertEqual(id(rel.aux_vars[1]), id(rel.relaxations.rel0.get_aux_var())) @@ -833,13 +906,19 @@ def test_div3(self): m.y = pe.Var(bounds=(1, 2)) m.z = pe.Var() m.w = pe.Var() - m.c = pe.Constraint(expr=m.z - m.x/m.y == 0) - m.c2 = pe.Constraint(expr=m.w - m.x/m.y == 0) + m.c = pe.Constraint(expr=m.z - m.x / m.y == 0) + m.c2 = pe.Constraint(expr=m.w - m.x / m.y == 0) rel = coramin.relaxations.relax(m, in_place=True, use_alpha_bb=False) self.assertIs(m, rel) relaxations = list(coramin.relaxations.relaxation_data_objects(m)) - constraints = list(coramin.relaxations.nonrelaxation_component_data_objects(m, ctype=pe.Constraint)) - vars = list(coramin.relaxations.nonrelaxation_component_data_objects(m, ctype=pe.Var)) + constraints = list( + coramin.relaxations.nonrelaxation_component_data_objects( + m, ctype=pe.Constraint + ) + ) + vars = list( + coramin.relaxations.nonrelaxation_component_data_objects(m, ctype=pe.Var) + ) self.assertEqual(len(relaxations), 1) self.assertEqual(len(constraints), 2) self.assertEqual(len(vars), 5) @@ -863,7 +942,7 @@ def test_div3(self): def _log_of_linear(x): - return pe.log(2*x + 1) + return pe.log(2 * x + 1) def _log_of_polynomial(x): @@ -903,17 +982,19 @@ def _pow_neg_3(x): def _pow_neg_point5(x): - return x**(-0.5) + return x ** (-0.5) def _pow_neg_1point2(x): - return x**(-1.2) + return x ** (-1.2) class TestUnivariate(unittest.TestCase): def helper(self, func, bounds_list): for relaxation_side in [ - RelaxationSide.UNDER, RelaxationSide.OVER, RelaxationSide.BOTH + RelaxationSide.UNDER, + RelaxationSide.OVER, + RelaxationSide.BOTH, ]: for simplification, use_alpha_bb, eigenvalue_bounder in [ (True, True, EigenValueBounder.Gershgorin), @@ -950,18 +1031,24 @@ def helper(self, func, bounds_list): m.x.fix(_x) m.aux.fix(pe.value(expr)) res = opt.solve(m) - self.assertEqual(res.termination_condition, - appsi.base.TerminationCondition.optimal) + self.assertEqual( + res.termination_condition, + appsi.base.TerminationCondition.optimal, + ) if relaxation_side == coramin.utils.RelaxationSide.UNDER: m.aux.fix(max(pe.value(func(lb)), pe.value(func(ub))) + 1) res = opt.solve(m) - self.assertEqual(res.termination_condition, - appsi.base.TerminationCondition.optimal) + self.assertEqual( + res.termination_condition, + appsi.base.TerminationCondition.optimal, + ) elif relaxation_side == coramin.utils.RelaxationSide.OVER: m.aux.fix(min(pe.value(func(lb)), pe.value(func(ub))) - 1) res = opt.solve(m) - self.assertEqual(res.termination_condition, - appsi.base.TerminationCondition.optimal) + self.assertEqual( + res.termination_condition, + appsi.base.TerminationCondition.optimal, + ) # ensure the relaxation is exact at the bounds of x m.aux.unfix() @@ -969,17 +1056,27 @@ def helper(self, func, bounds_list): m.obj = pe.Objective(expr=m.aux) for _x in [lb, ub]: m.x.fix(_x) - if relaxation_side in {coramin.utils.RelaxationSide.BOTH, coramin.utils.RelaxationSide.UNDER}: + if relaxation_side in { + coramin.utils.RelaxationSide.BOTH, + coramin.utils.RelaxationSide.UNDER, + }: m.obj.sense = pe.minimize res = opt.solve(m) - self.assertEqual(res.termination_condition, - appsi.base.TerminationCondition.optimal) + self.assertEqual( + res.termination_condition, + appsi.base.TerminationCondition.optimal, + ) self.assertAlmostEqual(m.aux.value, pe.value(func(_x))) - if relaxation_side in {coramin.utils.RelaxationSide.BOTH, coramin.utils.RelaxationSide.OVER}: + if relaxation_side in { + coramin.utils.RelaxationSide.BOTH, + coramin.utils.RelaxationSide.OVER, + }: m.obj.sense = pe.maximize res = opt.solve(m) - self.assertEqual(res.termination_condition, - appsi.base.TerminationCondition.optimal) + self.assertEqual( + res.termination_condition, + appsi.base.TerminationCondition.optimal, + ) self.assertAlmostEqual(m.aux.value, pe.value(func(_x)), 5) def test_exp(self): @@ -1066,6 +1163,7 @@ def test_log10(self): def test_quadratic(self): def func(x): return x**2 + self.helper(func=func, lb=-1, ub=2) def test_arctan(self): @@ -1085,13 +1183,20 @@ def helper(self, func, param_val): m.aux = pe.Var() m.p = pe.Param(mutable=True, initialize=param_val) m.c = pe.Constraint(expr=m.aux == func(m.p) * m.x**2) - self.assertIn(m.p, ComponentSet(identify_components(m.c.body, [_ParamData, ScalarParam]))) + self.assertIn( + m.p, ComponentSet(identify_components(m.c.body, [_ParamData, ScalarParam])) + ) coramin.relaxations.relax(m, in_place=True, use_alpha_bb=False) rels = list(coramin.relaxations.relaxation_data_objects(m)) self.assertEqual(len(rels), 1) r = rels[0] self.assertIsInstance(r, coramin.relaxations.PWXSquaredRelaxationData) - self.assertIn(m.p, ComponentSet(identify_components(m.aux_cons[1].body, [_ParamData, ScalarParam]))) + self.assertIn( + m.p, + ComponentSet( + identify_components(m.aux_cons[1].body, [_ParamData, ScalarParam]) + ), + ) def test_exp(self): self.helper(func=pe.exp, param_val=1) diff --git a/pyomo/contrib/coramin/relaxations/tests/test_copy.py b/pyomo/contrib/coramin/relaxations/tests/test_copy.py index 78be877093f..67567843046 100644 --- a/pyomo/contrib/coramin/relaxations/tests/test_copy.py +++ b/pyomo/contrib/coramin/relaxations/tests/test_copy.py @@ -1,4 +1,6 @@ -from pyomo.contrib.coramin.relaxations.copy_relaxation import copy_relaxation_with_local_data +from pyomo.contrib.coramin.relaxations.copy_relaxation import ( + copy_relaxation_with_local_data, +) from pyomo.common import unittest import pyomo.environ as pe from pyomo.contrib import coramin @@ -12,15 +14,19 @@ def test_quadratic(self): m.x = pe.Var(bounds=(-1, 1)) m.aux = pe.Var() m.rel = coramin.relaxations.PWXSquaredRelaxation() - m.rel.build(x=m.x, - aux_var=m.aux, - pw_repn='LOG', - relaxation_side=coramin.utils.RelaxationSide.OVER, - use_linear_relaxation=False) + m.rel.build( + x=m.x, + aux_var=m.aux, + pw_repn='LOG', + relaxation_side=coramin.utils.RelaxationSide.OVER, + use_linear_relaxation=False, + ) m2 = pe.ConcreteModel() m2.x = pe.Var(bounds=(-1, 1)) m2.aux = pe.Var() - new_rel = copy_relaxation_with_local_data(m.rel, {id(m.x): m2.x, id(m.aux): m2.aux}) + new_rel = copy_relaxation_with_local_data( + m.rel, {id(m.x): m2.x, id(m.aux): m2.aux} + ) self.assertIn(m2.x, ComponentSet(new_rel.get_rhs_vars())) self.assertEqual(len(new_rel.get_rhs_vars()), 1) self.assertEqual(m.rel.use_linear_relaxation, new_rel.use_linear_relaxation) @@ -35,15 +41,19 @@ def test_arctan(self): m.x = pe.Var(bounds=(-1, 1)) m.aux = pe.Var() m.rel = coramin.relaxations.PWArctanRelaxation() - m.rel.build(x=m.x, - aux_var=m.aux, - pw_repn='LOG', - relaxation_side=coramin.utils.RelaxationSide.OVER, - use_linear_relaxation=True) + m.rel.build( + x=m.x, + aux_var=m.aux, + pw_repn='LOG', + relaxation_side=coramin.utils.RelaxationSide.OVER, + use_linear_relaxation=True, + ) m2 = pe.ConcreteModel() m2.x = pe.Var(bounds=(-1, 1)) m2.aux = pe.Var() - new_rel = copy_relaxation_with_local_data(m.rel, {id(m.x): m2.x, id(m.aux): m2.aux}) + new_rel = copy_relaxation_with_local_data( + m.rel, {id(m.x): m2.x, id(m.aux): m2.aux} + ) self.assertIn(m2.x, ComponentSet(new_rel.get_rhs_vars())) self.assertEqual(len(new_rel.get_rhs_vars()), 1) self.assertEqual(m.rel.use_linear_relaxation, new_rel.use_linear_relaxation) @@ -58,15 +68,19 @@ def test_sin(self): m.x = pe.Var(bounds=(-1, 1)) m.aux = pe.Var() m.rel = coramin.relaxations.PWSinRelaxation() - m.rel.build(x=m.x, - aux_var=m.aux, - pw_repn='LOG', - relaxation_side=coramin.utils.RelaxationSide.OVER, - use_linear_relaxation=True) + m.rel.build( + x=m.x, + aux_var=m.aux, + pw_repn='LOG', + relaxation_side=coramin.utils.RelaxationSide.OVER, + use_linear_relaxation=True, + ) m2 = pe.ConcreteModel() m2.x = pe.Var(bounds=(-1, 1)) m2.aux = pe.Var() - new_rel = copy_relaxation_with_local_data(m.rel, {id(m.x): m2.x, id(m.aux): m2.aux}) + new_rel = copy_relaxation_with_local_data( + m.rel, {id(m.x): m2.x, id(m.aux): m2.aux} + ) self.assertIn(m2.x, ComponentSet(new_rel.get_rhs_vars())) self.assertEqual(len(new_rel.get_rhs_vars()), 1) self.assertEqual(m.rel.use_linear_relaxation, new_rel.use_linear_relaxation) @@ -81,15 +95,19 @@ def test_cos(self): m.x = pe.Var(bounds=(-1, 1)) m.aux = pe.Var() m.rel = coramin.relaxations.PWCosRelaxation() - m.rel.build(x=m.x, - aux_var=m.aux, - pw_repn='LOG', - relaxation_side=coramin.utils.RelaxationSide.UNDER, - use_linear_relaxation=True) + m.rel.build( + x=m.x, + aux_var=m.aux, + pw_repn='LOG', + relaxation_side=coramin.utils.RelaxationSide.UNDER, + use_linear_relaxation=True, + ) m2 = pe.ConcreteModel() m2.x = pe.Var(bounds=(-1, 1)) m2.aux = pe.Var() - new_rel = copy_relaxation_with_local_data(m.rel, {id(m.x): m2.x, id(m.aux): m2.aux}) + new_rel = copy_relaxation_with_local_data( + m.rel, {id(m.x): m2.x, id(m.aux): m2.aux} + ) self.assertIn(m2.x, ComponentSet(new_rel.get_rhs_vars())) self.assertEqual(len(new_rel.get_rhs_vars()), 1) self.assertEqual(m.rel.use_linear_relaxation, new_rel.use_linear_relaxation) @@ -104,17 +122,21 @@ def test_exp(self): m.x = pe.Var(bounds=(-1, 1)) m.aux = pe.Var() m.rel = coramin.relaxations.PWUnivariateRelaxation() - m.rel.build(x=m.x, - aux_var=m.aux, - shape=coramin.utils.FunctionShape.CONVEX, - f_x_expr=pe.exp(m.x), - pw_repn='LOG', - relaxation_side=coramin.utils.RelaxationSide.UNDER, - use_linear_relaxation=True) + m.rel.build( + x=m.x, + aux_var=m.aux, + shape=coramin.utils.FunctionShape.CONVEX, + f_x_expr=pe.exp(m.x), + pw_repn='LOG', + relaxation_side=coramin.utils.RelaxationSide.UNDER, + use_linear_relaxation=True, + ) m2 = pe.ConcreteModel() m2.x = pe.Var(bounds=(-1, 1)) m2.aux = pe.Var() - new_rel = copy_relaxation_with_local_data(m.rel, {id(m.x): m2.x, id(m.aux): m2.aux}) + new_rel = copy_relaxation_with_local_data( + m.rel, {id(m.x): m2.x, id(m.aux): m2.aux} + ) self.assertIn(m2.x, ComponentSet(new_rel.get_rhs_vars())) self.assertEqual(len(new_rel.get_rhs_vars()), 1) self.assertEqual(m.rel.use_linear_relaxation, new_rel.use_linear_relaxation) @@ -129,17 +151,21 @@ def test_log(self): m.x = pe.Var(bounds=(0.5, 1.5)) m.aux = pe.Var() m.rel = coramin.relaxations.PWUnivariateRelaxation() - m.rel.build(x=m.x, - aux_var=m.aux, - shape=coramin.utils.FunctionShape.CONCAVE, - f_x_expr=pe.log(m.x), - pw_repn='LOG', - relaxation_side=coramin.utils.RelaxationSide.UNDER, - use_linear_relaxation=True) + m.rel.build( + x=m.x, + aux_var=m.aux, + shape=coramin.utils.FunctionShape.CONCAVE, + f_x_expr=pe.log(m.x), + pw_repn='LOG', + relaxation_side=coramin.utils.RelaxationSide.UNDER, + use_linear_relaxation=True, + ) m2 = pe.ConcreteModel() m2.x = pe.Var(bounds=(0.5, 1.5)) m2.aux = pe.Var() - new_rel = copy_relaxation_with_local_data(m.rel, {id(m.x): m2.x, id(m.aux): m2.aux}) + new_rel = copy_relaxation_with_local_data( + m.rel, {id(m.x): m2.x, id(m.aux): m2.aux} + ) self.assertIn(m2.x, ComponentSet(new_rel.get_rhs_vars())) self.assertEqual(len(new_rel.get_rhs_vars()), 1) self.assertEqual(m.rel.use_linear_relaxation, new_rel.use_linear_relaxation) @@ -155,17 +181,19 @@ def test_multivariate(self): m.y = pe.Var(bounds=(-1, 1)) m.aux = pe.Var() m.rel = coramin.relaxations.MultivariateRelaxation() - m.rel.build(aux_var=m.aux, - shape=coramin.utils.FunctionShape.CONVEX, - f_x_expr=(m.x**2 + m.y**2), - use_linear_relaxation=True) + m.rel.build( + aux_var=m.aux, + shape=coramin.utils.FunctionShape.CONVEX, + f_x_expr=(m.x**2 + m.y**2), + use_linear_relaxation=True, + ) m2 = pe.ConcreteModel() m2.x = pe.Var(bounds=(-1, 1)) m2.y = pe.Var(bounds=(-1, 1)) m2.aux = pe.Var() - new_rel = copy_relaxation_with_local_data(m.rel, {id(m.x): m2.x, - id(m.y): m2.y, - id(m.aux): m2.aux}) + new_rel = copy_relaxation_with_local_data( + m.rel, {id(m.x): m2.x, id(m.y): m2.y, id(m.aux): m2.aux} + ) self.assertEqual(m.rel.use_linear_relaxation, new_rel.use_linear_relaxation) self.assertEqual(m.rel.relaxation_side, new_rel.relaxation_side) rhs_vars = ComponentSet(new_rel.get_rhs_vars()) @@ -175,7 +203,9 @@ def test_multivariate(self): self.assertIs(m2.aux, new_rel.get_aux_var()) self.assertEqual(m.rel._function_shape, new_rel._function_shape) self.assertIsInstance(new_rel, coramin.relaxations.MultivariateRelaxationData) - self.assertEqual(sympyify_expression(m2.x**2 + m2.y**2 - new_rel.get_rhs_expr())[1], 0) + self.assertEqual( + sympyify_expression(m2.x**2 + m2.y**2 - new_rel.get_rhs_expr())[1], 0 + ) def test_multivariate2(self): m = pe.ConcreteModel() @@ -183,17 +213,19 @@ def test_multivariate2(self): m.y = pe.Var(bounds=(-1, 1)) m.aux = pe.Var() m.rel = coramin.relaxations.MultivariateRelaxation() - m.rel.build(aux_var=m.aux, - shape=coramin.utils.FunctionShape.CONCAVE, - f_x_expr=(-m.x**2 - m.y**2), - use_linear_relaxation=True) + m.rel.build( + aux_var=m.aux, + shape=coramin.utils.FunctionShape.CONCAVE, + f_x_expr=(-m.x**2 - m.y**2), + use_linear_relaxation=True, + ) m2 = pe.ConcreteModel() m2.x = pe.Var(bounds=(-1, 1)) m2.y = pe.Var(bounds=(-1, 1)) m2.aux = pe.Var() - new_rel = copy_relaxation_with_local_data(m.rel, {id(m.x): m2.x, - id(m.y): m2.y, - id(m.aux): m2.aux}) + new_rel = copy_relaxation_with_local_data( + m.rel, {id(m.x): m2.x, id(m.y): m2.y, id(m.aux): m2.aux} + ) self.assertEqual(m.rel.use_linear_relaxation, new_rel.use_linear_relaxation) self.assertEqual(m.rel.relaxation_side, new_rel.relaxation_side) rhs_vars = ComponentSet(new_rel.get_rhs_vars()) @@ -203,4 +235,6 @@ def test_multivariate2(self): self.assertIs(m2.aux, new_rel.get_aux_var()) self.assertEqual(m.rel._function_shape, new_rel._function_shape) self.assertIsInstance(new_rel, coramin.relaxations.MultivariateRelaxationData) - self.assertEqual(sympyify_expression(-m2.x**2 - m2.y**2 - new_rel.get_rhs_expr())[1], 0) + self.assertEqual( + sympyify_expression(-m2.x**2 - m2.y**2 - new_rel.get_rhs_expr())[1], 0 + ) diff --git a/pyomo/contrib/coramin/relaxations/tests/test_iterators.py b/pyomo/contrib/coramin/relaxations/tests/test_iterators.py index c248784b0ce..d34c742aa39 100644 --- a/pyomo/contrib/coramin/relaxations/tests/test_iterators.py +++ b/pyomo/contrib/coramin/relaxations/tests/test_iterators.py @@ -11,10 +11,12 @@ def setUp(self): m.y = pe.Var() m.c1 = pe.Constraint(expr=m.y == m.x) m.r1 = coramin.relaxations.PWUnivariateRelaxation() - m.r1.set_input(x=m.x, - aux_var=m.y, - shape=coramin.utils.FunctionShape.CONCAVE, - f_x_expr=pe.log(m.x)) + m.r1.set_input( + x=m.x, + aux_var=m.y, + shape=coramin.utils.FunctionShape.CONCAVE, + f_x_expr=pe.log(m.x), + ) m.r1.add_partition_point(value=1) m.r1.rebuild() m.b1 = pe.Block() @@ -22,10 +24,12 @@ def setUp(self): m.b1.y = pe.Var() m.b1.c1 = pe.Constraint(expr=m.b1.y == m.b1.x) m.b1.r1 = coramin.relaxations.PWUnivariateRelaxation() - m.b1.r1.set_input(x=m.b1.x, - aux_var=m.b1.y, - shape=coramin.utils.FunctionShape.CONCAVE, - f_x_expr=pe.log(m.b1.x)) + m.b1.r1.set_input( + x=m.b1.x, + aux_var=m.b1.y, + shape=coramin.utils.FunctionShape.CONCAVE, + f_x_expr=pe.log(m.b1.x), + ) m.b1.r1.add_partition_point(value=1) m.b1.r1.rebuild() m.b1.b1 = pe.Block() @@ -34,18 +38,30 @@ def setUp(self): def test_relaxation_data_objects(self): m = self.m - rels = list(coramin.relaxations.relaxation_data_objects(m, descend_into=True, active=True)) + rels = list( + coramin.relaxations.relaxation_data_objects( + m, descend_into=True, active=True + ) + ) self.assertEqual(len(rels), 2) self.assertIn(m.r1, rels) self.assertIn(m.b1.r1, rels) m.r1.deactivate() - rels = list(coramin.relaxations.relaxation_data_objects(m, descend_into=True, active=True)) + rels = list( + coramin.relaxations.relaxation_data_objects( + m, descend_into=True, active=True + ) + ) self.assertEqual(len(rels), 1) self.assertNotIn(m.r1, rels) self.assertIn(m.b1.r1, rels) - rels = list(coramin.relaxations.relaxation_data_objects(m, descend_into=True, active=None)) + rels = list( + coramin.relaxations.relaxation_data_objects( + m, descend_into=True, active=None + ) + ) self.assertEqual(len(rels), 2) self.assertIn(m.r1, rels) self.assertIn(m.b1.r1, rels) @@ -59,29 +75,37 @@ def test_relaxation_data_objects(self): def test_nonrelaxation_component_data_objects(self): m = self.m all_vars = list(m.component_data_objects(pe.Var, descend_into=True)) - non_relaxation_vars = list(coramin.relaxations.nonrelaxation_component_data_objects(m, - ctype=pe.Var, - descend_into=True)) + non_relaxation_vars = list( + coramin.relaxations.nonrelaxation_component_data_objects( + m, ctype=pe.Var, descend_into=True + ) + ) self.assertEqual(len(non_relaxation_vars), 4) self.assertGreater(len(all_vars), 4) all_vars = list(m.component_data_objects(pe.Var, descend_into=False)) - non_relaxation_vars = list(coramin.relaxations.nonrelaxation_component_data_objects(m, - ctype=pe.Var, - descend_into=False)) + non_relaxation_vars = list( + coramin.relaxations.nonrelaxation_component_data_objects( + m, ctype=pe.Var, descend_into=False + ) + ) self.assertEqual(len(non_relaxation_vars), 2) self.assertEqual(len(all_vars), 2) all_blocks = list(m.component_data_objects(pe.Block, descend_into=True)) - non_relaxation_blocks = list(coramin.relaxations.nonrelaxation_component_data_objects(m, - ctype=pe.Block, - descend_into=True)) + non_relaxation_blocks = list( + coramin.relaxations.nonrelaxation_component_data_objects( + m, ctype=pe.Block, descend_into=True + ) + ) self.assertEqual(len(non_relaxation_blocks), 2) self.assertEqual(len(all_blocks), 8) all_blocks = list(m.component_data_objects(pe.Block, descend_into=False)) - non_relaxation_blocks = list(coramin.relaxations.nonrelaxation_component_data_objects(m, - ctype=pe.Block, - descend_into=False)) + non_relaxation_blocks = list( + coramin.relaxations.nonrelaxation_component_data_objects( + m, ctype=pe.Block, descend_into=False + ) + ) self.assertEqual(len(non_relaxation_blocks), 1) self.assertEqual(len(all_blocks), 2) diff --git a/pyomo/contrib/coramin/relaxations/tests/test_mccormick.py b/pyomo/contrib/coramin/relaxations/tests/test_mccormick.py index efb520e5891..4cfafa5e832 100644 --- a/pyomo/contrib/coramin/relaxations/tests/test_mccormick.py +++ b/pyomo/contrib/coramin/relaxations/tests/test_mccormick.py @@ -36,8 +36,10 @@ def test_mccormick2(self): model.obj = pyo.Objective(expr=-model.w - 2 * model.x) model.con = pyo.Constraint(expr=model.w <= 12) + def mc_rule(b): b.build(x1=model.x, x2=model.y, aux_var=model.w) + model.mc = coramin.relaxations.PWMcCormickRelaxation(rule=mc_rule) linsolver = pyo.SolverFactory('gurobi_direct') @@ -57,6 +59,7 @@ def test_mccormick3_BOTH(self): def mc_rule(b): m = b.parent_block() b.build(x1=m.x, x2=m.y, aux_var=m.w) + model.mc = coramin.relaxations.PWMcCormickRelaxation(rule=mc_rule) linsolver = pyo.SolverFactory('gurobi_direct', tee=True) @@ -70,12 +73,18 @@ def test_mccormick3_OVER(self): model.y = pyo.Var(bounds=(0, 3)) model.w = pyo.Var() - model.obj = pyo.Objective(expr=-model.w + 0.1*model.x + 0.1*model.y) + model.obj = pyo.Objective(expr=-model.w + 0.1 * model.x + 0.1 * model.y) model.con = pyo.Constraint(expr=model.w <= 12) def mc_rule(b): m = b.parent_block() - b.build(x1=m.x, x2=m.y, aux_var=m.w, relaxation_side=coramin.utils.RelaxationSide.OVER) + b.build( + x1=m.x, + x2=m.y, + aux_var=m.w, + relaxation_side=coramin.utils.RelaxationSide.OVER, + ) + model.mc = coramin.relaxations.PWMcCormickRelaxation(rule=mc_rule) linsolver = pyo.SolverFactory('gurobi_direct') @@ -94,7 +103,13 @@ def test_mccormick3_UNDER(self): def mc_rule(b): m = b.parent_block() - b.build(x1=m.x, x2=m.y, aux_var=m.w, relaxation_side=coramin.utils.RelaxationSide.UNDER) + b.build( + x1=m.x, + x2=m.y, + aux_var=m.w, + relaxation_side=coramin.utils.RelaxationSide.UNDER, + ) + model.mc = coramin.relaxations.PWMcCormickRelaxation(rule=mc_rule) linsolver = pyo.SolverFactory('gurobi_direct', tee=True) diff --git a/pyomo/contrib/coramin/relaxations/tests/test_relaxations.py b/pyomo/contrib/coramin/relaxations/tests/test_relaxations.py index 21a930e27ed..5eab5e7f257 100644 --- a/pyomo/contrib/coramin/relaxations/tests/test_relaxations.py +++ b/pyomo/contrib/coramin/relaxations/tests/test_relaxations.py @@ -19,7 +19,9 @@ import io -def _grid_rhs_vars(v_list: Sequence[_GeneralVarData], num_points: int = 30) -> List[Tuple[float, ...]]: +def _grid_rhs_vars( + v_list: Sequence[_GeneralVarData], num_points: int = 30 +) -> List[Tuple[float, ...]]: res = list() for v in v_list: res.append(np.linspace(v.lb, v.ub, num_points)) @@ -27,9 +29,11 @@ def _grid_rhs_vars(v_list: Sequence[_GeneralVarData], num_points: int = 30) -> L return res -def _get_rhs_vals(rhs_vars: Sequence[_GeneralVarData], - rhs_expr: ExpressionBase, - eval_pts: List[Tuple[float, ...]]) -> List[float]: +def _get_rhs_vals( + rhs_vars: Sequence[_GeneralVarData], + rhs_expr: ExpressionBase, + eval_pts: List[Tuple[float, ...]], +) -> List[float]: rhs_vals = list() for pt in eval_pts: for v, p in zip(rhs_vars, pt): @@ -40,13 +44,15 @@ def _get_rhs_vals(rhs_vars: Sequence[_GeneralVarData], return rhs_vals -def _get_relaxation_vals(rhs_vars: Sequence[_GeneralVarData], - rhs_expr: ExpressionBase, - m: _BlockData, - rel: coramin.relaxations.BaseRelaxationData, - eval_pts: List[Tuple[float, ...]], - rel_side: coramin.utils.RelaxationSide, - linear: bool = True) -> List[float]: +def _get_relaxation_vals( + rhs_vars: Sequence[_GeneralVarData], + rhs_expr: ExpressionBase, + m: _BlockData, + rel: coramin.relaxations.BaseRelaxationData, + eval_pts: List[Tuple[float, ...]], + rel_side: coramin.utils.RelaxationSide, + linear: bool = True, +) -> List[float]: opt = appsi.solvers.Gurobi() opt.update_config.update_vars = True opt.update_config.check_for_new_or_removed_vars = False @@ -81,14 +87,18 @@ def _get_relaxation_vals(rhs_vars: Sequence[_GeneralVarData], def _num_cons(rel): - cons = list(rel.component_data_objects(pe.Constraint, descend_into=True, active=True)) + cons = list( + rel.component_data_objects(pe.Constraint, descend_into=True, active=True) + ) return len(cons) -def _check_unbounded(m: _BlockData, - rel: coramin.relaxations.BaseRelaxationData, - rel_side: coramin.utils.RelaxationSide, - linear: bool = True): +def _check_unbounded( + m: _BlockData, + rel: coramin.relaxations.BaseRelaxationData, + rel_side: coramin.utils.RelaxationSide, + linear: bool = True, +): if rel_side == coramin.utils.RelaxationSide.UNDER: sense = pe.minimize else: @@ -97,7 +107,7 @@ def _check_unbounded(m: _BlockData, for v in rel.get_rhs_vars(): if v.has_lb() and v.has_ub(): - v.fix(0.5*(v.lb + v.ub)) + v.fix(0.5 * (v.lb + v.ub)) elif v.has_lb(): v.fix(v.lb + 0.1) elif v.has_ub(): @@ -136,7 +146,7 @@ def _check_linear_or_convex(rel: coramin.relaxations.BaseRelaxationData): e = repn.constant for coef, v in zip(repn.linear_coefs, repn.linear_vars): if v is not rel.get_aux_var(): - e += coef*v + e += coef * v e += repn.nonlinear_expr # this will only work if all the off-diagonal elements of the hessian are 0 @@ -169,15 +179,28 @@ def _check_scaling(m: _BlockData, rel: coramin.relaxations.BaseRelaxationData) - cons_with_large_coefs = dict() cons_with_small_coefs = dict() for c in m.component_data_objects(pe.Constraint, descend_into=True, active=True): - _check_coefficients(c, c.body, rel.large_coef, rel.small_coef, cons_with_large_coefs, cons_with_small_coefs) + _check_coefficients( + c, + c.body, + rel.large_coef, + rel.small_coef, + cons_with_large_coefs, + cons_with_small_coefs, + ) passed = len(cons_with_large_coefs) == 0 and len(cons_with_small_coefs) == 0 return passed class TestRelaxationBasics(unittest.TestCase): - def valid_relaxation_helper(self, m: _BlockData, rel: coramin.relaxations.BaseRelaxationData, - rhs_expr: ExpressionBase, num_points: int = 30, check_underestimator: bool = True, - check_overestimator: bool = True): + def valid_relaxation_helper( + self, + m: _BlockData, + rel: coramin.relaxations.BaseRelaxationData, + rhs_expr: ExpressionBase, + num_points: int = 30, + check_underestimator: bool = True, + check_overestimator: bool = True, + ): if rel.use_linear_relaxation: self.assertTrue(_check_linear(m)) rhs_vars = rel.get_rhs_vars() @@ -186,38 +209,76 @@ def valid_relaxation_helper(self, m: _BlockData, rel: coramin.relaxations.BaseRe rhs_vals = np.array(rhs_vals) if check_underestimator: - under_est_vals = _get_relaxation_vals(rhs_vars, rhs_expr, m, rel, sample_points, - coramin.utils.RelaxationSide.UNDER) + under_est_vals = _get_relaxation_vals( + rhs_vars, + rhs_expr, + m, + rel, + sample_points, + coramin.utils.RelaxationSide.UNDER, + ) under_est_vals = np.array(under_est_vals) self.assertTrue(np.all(rhs_vals >= under_est_vals)) if check_overestimator: - over_est_vals = _get_relaxation_vals(rhs_vars, rhs_expr, m, rel, sample_points, - coramin.utils.RelaxationSide.OVER) + over_est_vals = _get_relaxation_vals( + rhs_vars, + rhs_expr, + m, + rel, + sample_points, + coramin.utils.RelaxationSide.OVER, + ) over_est_vals = np.array(over_est_vals) self.assertTrue(np.all(rhs_vals <= over_est_vals)) - def equal_at_points_helper(self, m: _BlockData, rel: coramin.relaxations.BaseRelaxationData, - rhs_expr: ExpressionBase, pts: Sequence[Tuple[float, ...]], - check_underestimator: bool = True, check_overestimator: bool = True, - linear: bool = True): + def equal_at_points_helper( + self, + m: _BlockData, + rel: coramin.relaxations.BaseRelaxationData, + rhs_expr: ExpressionBase, + pts: Sequence[Tuple[float, ...]], + check_underestimator: bool = True, + check_overestimator: bool = True, + linear: bool = True, + ): rhs_vars = rel.get_rhs_vars() rhs_vals = _get_rhs_vals(rhs_vars, rhs_expr, pts) rhs_vals = np.array(rhs_vals) if check_underestimator: - under_est_vals = _get_relaxation_vals(rhs_vars, rhs_expr, m, rel, pts, - coramin.utils.RelaxationSide.UNDER, linear) + under_est_vals = _get_relaxation_vals( + rhs_vars, + rhs_expr, + m, + rel, + pts, + coramin.utils.RelaxationSide.UNDER, + linear, + ) under_est_vals = np.array(under_est_vals) self.assertTrue(np.all(np.isclose(rhs_vals, under_est_vals))) if check_overestimator: - over_est_vals = _get_relaxation_vals(rhs_vars, rhs_expr, m, rel, pts, - coramin.utils.RelaxationSide.OVER, linear) + over_est_vals = _get_relaxation_vals( + rhs_vars, + rhs_expr, + m, + rel, + pts, + coramin.utils.RelaxationSide.OVER, + linear, + ) over_est_vals = np.array(over_est_vals) self.assertTrue(np.all(np.isclose(rhs_vals, over_est_vals))) - def nonlinear_relaxation_helper(self, m: _BlockData, rel: coramin.relaxations.BaseRelaxationData, - rhs_expr: ExpressionBase, num_points: int = 30, - supports_underestimator: bool = True, supports_overestimator: bool = True, - check_equal_at_points: bool = True): + def nonlinear_relaxation_helper( + self, + m: _BlockData, + rel: coramin.relaxations.BaseRelaxationData, + rhs_expr: ExpressionBase, + num_points: int = 30, + supports_underestimator: bool = True, + supports_overestimator: bool = True, + check_equal_at_points: bool = True, + ): rel.use_linear_relaxation = False rel.rebuild() if rel.is_rhs_convex() or rel.is_rhs_concave(): @@ -231,16 +292,30 @@ def nonlinear_relaxation_helper(self, m: _BlockData, rel: coramin.relaxations.Ba rhs_vals = np.array(rhs_vals) if supports_underestimator: - under_est_vals = _get_relaxation_vals(rhs_vars, rhs_expr, m, rel, sample_points, - coramin.utils.RelaxationSide.UNDER, linear=False) + under_est_vals = _get_relaxation_vals( + rhs_vars, + rhs_expr, + m, + rel, + sample_points, + coramin.utils.RelaxationSide.UNDER, + linear=False, + ) under_est_vals = np.array(under_est_vals) if rel.is_rhs_convex() and check_equal_at_points: self.assertTrue(np.all(np.isclose(rhs_vals, under_est_vals))) else: self.assertTrue(np.all(rhs_vals >= under_est_vals)) if supports_overestimator: - over_est_vals = _get_relaxation_vals(rhs_vars, rhs_expr, m, rel, sample_points, - coramin.utils.RelaxationSide.OVER, linear=False) + over_est_vals = _get_relaxation_vals( + rhs_vars, + rhs_expr, + m, + rel, + sample_points, + coramin.utils.RelaxationSide.OVER, + linear=False, + ) over_est_vals = np.array(over_est_vals) if rel.is_rhs_concave() and check_equal_at_points: self.assertTrue(np.all(np.isclose(rhs_vals, over_est_vals))) @@ -260,9 +335,15 @@ def nonlinear_relaxation_helper(self, m: _BlockData, rel: coramin.relaxations.Ba rel.use_linear_relaxation = True rel.rebuild() - def original_constraint_helper(self, m: _BlockData, rel: coramin.relaxations.BaseRelaxationData, - rhs_expr: ExpressionBase, num_points: int = 15, supports_underestimator: bool = True, - supports_overestimator: bool = True): + def original_constraint_helper( + self, + m: _BlockData, + rel: coramin.relaxations.BaseRelaxationData, + rhs_expr: ExpressionBase, + num_points: int = 15, + supports_underestimator: bool = True, + supports_overestimator: bool = True, + ): rel.rebuild(build_nonlinear_constraint=True) self.assertFalse(_check_linear(m)) rhs_vars = rel.get_rhs_vars() @@ -271,24 +352,53 @@ def original_constraint_helper(self, m: _BlockData, rel: coramin.relaxations.Bas rhs_vals = np.array(rhs_vals) if supports_underestimator: - under_est_vals = _get_relaxation_vals(rhs_vars, rhs_expr, m, rel, sample_points, - coramin.utils.RelaxationSide.UNDER, linear=False) + under_est_vals = _get_relaxation_vals( + rhs_vars, + rhs_expr, + m, + rel, + sample_points, + coramin.utils.RelaxationSide.UNDER, + linear=False, + ) under_est_vals = np.array(under_est_vals) self.assertTrue(np.all(np.isclose(rhs_vals, under_est_vals))) if supports_overestimator: - over_est_vals = _get_relaxation_vals(rhs_vars, rhs_expr, m, rel, sample_points, - coramin.utils.RelaxationSide.OVER, linear=False) + over_est_vals = _get_relaxation_vals( + rhs_vars, + rhs_expr, + m, + rel, + sample_points, + coramin.utils.RelaxationSide.OVER, + linear=False, + ) over_est_vals = np.array(over_est_vals) self.assertTrue(np.all(np.isclose(rhs_vals, over_est_vals))) rel.rebuild() - self.valid_relaxation_helper(m, rel, rhs_expr, num_points, supports_underestimator, supports_overestimator) + self.valid_relaxation_helper( + m, + rel, + rhs_expr, + num_points, + supports_underestimator, + supports_overestimator, + ) - def relaxation_side_helper(self, m: _BlockData, rel: coramin.relaxations.BaseRelaxationData, - rhs_expr: ExpressionBase, check_nonlinear_relaxation: bool = True): + def relaxation_side_helper( + self, + m: _BlockData, + rel: coramin.relaxations.BaseRelaxationData, + rhs_expr: ExpressionBase, + check_nonlinear_relaxation: bool = True, + ): rel.relaxation_side = coramin.utils.RelaxationSide.UNDER rel.rebuild() - sample_points = [tuple(v.lb for v in rel.get_rhs_vars()), tuple(v.ub for v in rel.get_rhs_vars())] + sample_points = [ + tuple(v.lb for v in rel.get_rhs_vars()), + tuple(v.ub for v in rel.get_rhs_vars()), + ] self.equal_at_points_helper(m, rel, rhs_expr, sample_points, True, False) self.assertTrue(_check_unbounded(m, rel, coramin.RelaxationSide.OVER)) @@ -303,13 +413,21 @@ def relaxation_side_helper(self, m: _BlockData, rel: coramin.relaxations.BaseRel rel.relaxation_side = coramin.utils.RelaxationSide.UNDER rel.rebuild() sample_points = [(v.lb, v.ub) for v in rel.get_rhs_vars()] - self.equal_at_points_helper(m, rel, rhs_expr, sample_points, True, False, False) - self.assertTrue(_check_unbounded(m, rel, coramin.RelaxationSide.OVER, False)) + self.equal_at_points_helper( + m, rel, rhs_expr, sample_points, True, False, False + ) + self.assertTrue( + _check_unbounded(m, rel, coramin.RelaxationSide.OVER, False) + ) rel.relaxation_side = coramin.utils.RelaxationSide.OVER rel.rebuild() - self.equal_at_points_helper(m, rel, rhs_expr, sample_points, False, True, False) - self.assertTrue(_check_unbounded(m, rel, coramin.RelaxationSide.UNDER, False)) + self.equal_at_points_helper( + m, rel, rhs_expr, sample_points, False, True, False + ) + self.assertTrue( + _check_unbounded(m, rel, coramin.RelaxationSide.UNDER, False) + ) rel.relaxation_side = coramin.utils.RelaxationSide.UNDER rel.rebuild(build_nonlinear_constraint=True) @@ -326,9 +444,16 @@ def relaxation_side_helper(self, m: _BlockData, rel: coramin.relaxations.BaseRel rel.relaxation_side = coramin.RelaxationSide.BOTH rel.rebuild() - def changing_bounds_helper(self, m: _BlockData, rel: coramin.relaxations.BaseRelaxationData, - rhs_expr: ExpressionBase, num_points: int = 10, supports_underestimator: bool = True, - supports_overestimator: bool = True, check_equal_at_points: bool = True): + def changing_bounds_helper( + self, + m: _BlockData, + rel: coramin.relaxations.BaseRelaxationData, + rhs_expr: ExpressionBase, + num_points: int = 10, + supports_underestimator: bool = True, + supports_overestimator: bool = True, + check_equal_at_points: bool = True, + ): rhs_vars = rel.get_rhs_vars() orig_bnds = pe.ComponentMap((v, (v.lb, v.ub)) for v in rhs_vars) grid_pts = _grid_rhs_vars(rhs_vars, num_points=num_points) @@ -337,46 +462,109 @@ def changing_bounds_helper(self, m: _BlockData, rel: coramin.relaxations.BaseRel v.setlb(p) rel.rebuild() self.assertLessEqual(_num_cons(rel), 4) - self.valid_relaxation_helper(m, rel, rhs_expr, num_points, supports_underestimator, supports_overestimator) - self.equal_at_points_helper(m, rel, rhs_expr, - [tuple(v.lb for v in rhs_vars), tuple(v.ub for v in rhs_vars)], - supports_underestimator, supports_overestimator) + self.valid_relaxation_helper( + m, + rel, + rhs_expr, + num_points, + supports_underestimator, + supports_overestimator, + ) + self.equal_at_points_helper( + m, + rel, + rhs_expr, + [tuple(v.lb for v in rhs_vars), tuple(v.ub for v in rhs_vars)], + supports_underestimator, + supports_overestimator, + ) if rel.is_rhs_convex() or rel.is_rhs_concave(): - self.nonlinear_relaxation_helper(m, rel, rhs_expr, num_points, - supports_underestimator, supports_overestimator, - check_equal_at_points) + self.nonlinear_relaxation_helper( + m, + rel, + rhs_expr, + num_points, + supports_underestimator, + supports_overestimator, + check_equal_at_points, + ) for v, (v_lb, v_ub) in orig_bnds.items(): v.setlb(v_lb) v.setub(v_ub) rel.rebuild() self.assertLessEqual(_num_cons(rel), 4) - self.valid_relaxation_helper(m, rel, rhs_expr, num_points, supports_underestimator, supports_overestimator) - self.equal_at_points_helper(m, rel, rhs_expr, [tuple(v.lb for v in rhs_vars), tuple(v.ub for v in rhs_vars)], - supports_underestimator, supports_overestimator) + self.valid_relaxation_helper( + m, + rel, + rhs_expr, + num_points, + supports_underestimator, + supports_overestimator, + ) + self.equal_at_points_helper( + m, + rel, + rhs_expr, + [tuple(v.lb for v in rhs_vars), tuple(v.ub for v in rhs_vars)], + supports_underestimator, + supports_overestimator, + ) for pt in grid_pts: for v, p in zip(rhs_vars, pt): v.setub(p) rel.rebuild() self.assertLessEqual(_num_cons(rel), 4) - self.valid_relaxation_helper(m, rel, rhs_expr, num_points, - supports_underestimator, supports_overestimator) - self.equal_at_points_helper(m, rel, rhs_expr, - [tuple(v.lb for v in rhs_vars), tuple(v.ub for v in rhs_vars)], - supports_underestimator, supports_overestimator) + self.valid_relaxation_helper( + m, + rel, + rhs_expr, + num_points, + supports_underestimator, + supports_overestimator, + ) + self.equal_at_points_helper( + m, + rel, + rhs_expr, + [tuple(v.lb for v in rhs_vars), tuple(v.ub for v in rhs_vars)], + supports_underestimator, + supports_overestimator, + ) if rel.is_rhs_convex() or rel.is_rhs_concave(): - self.nonlinear_relaxation_helper(m, rel, rhs_expr, num_points, - supports_underestimator, supports_overestimator, - check_equal_at_points) + self.nonlinear_relaxation_helper( + m, + rel, + rhs_expr, + num_points, + supports_underestimator, + supports_overestimator, + check_equal_at_points, + ) for v, (v_lb, v_ub) in orig_bnds.items(): v.setlb(v_lb) v.setub(v_ub) rel.rebuild() self.assertLessEqual(_num_cons(rel), 4) - self.valid_relaxation_helper(m, rel, rhs_expr, num_points, supports_underestimator, supports_overestimator) - self.equal_at_points_helper(m, rel, rhs_expr, [tuple(v.lb for v in rhs_vars), tuple(v.ub for v in rhs_vars)], - supports_underestimator, supports_overestimator) + self.valid_relaxation_helper( + m, + rel, + rhs_expr, + num_points, + supports_underestimator, + supports_overestimator, + ) + self.equal_at_points_helper( + m, + rel, + rhs_expr, + [tuple(v.lb for v in rhs_vars), tuple(v.ub for v in rhs_vars)], + supports_underestimator, + supports_overestimator, + ) - def large_bounds_helper(self, m: _BlockData, rel: coramin.relaxations.BaseRelaxationData, lb=1, ub=1e6): + def large_bounds_helper( + self, m: _BlockData, rel: coramin.relaxations.BaseRelaxationData, lb=1, ub=1e6 + ): orig_bnds = pe.ComponentMap((v, (v.lb, v.ub)) for v in rel.get_rhs_vars()) for v in rel.get_rhs_vars(): @@ -390,9 +578,13 @@ def large_bounds_helper(self, m: _BlockData, rel: coramin.relaxations.BaseRelaxa if rel.is_rhs_convex(): self.assertTrue(_check_unbounded(m, rel, coramin.utils.RelaxationSide.OVER)) elif rel.is_rhs_concave(): - self.assertTrue(_check_unbounded(m, rel, coramin.utils.RelaxationSide.UNDER)) + self.assertTrue( + _check_unbounded(m, rel, coramin.utils.RelaxationSide.UNDER) + ) else: - self.assertTrue(_check_unbounded(m, rel, coramin.utils.RelaxationSide.UNDER)) + self.assertTrue( + _check_unbounded(m, rel, coramin.utils.RelaxationSide.UNDER) + ) self.assertTrue(_check_unbounded(m, rel, coramin.utils.RelaxationSide.OVER)) for v, (v_lb, v_ub) in orig_bnds.items(): @@ -400,14 +592,23 @@ def large_bounds_helper(self, m: _BlockData, rel: coramin.relaxations.BaseRelaxa v.setub(v_ub) rel.rebuild() - def infinite_bounds_helper(self, m: _BlockData, rel: coramin.relaxations.BaseRelaxationData): + def infinite_bounds_helper( + self, m: _BlockData, rel: coramin.relaxations.BaseRelaxationData + ): self.large_bounds_helper(m, rel, None, None) self.large_bounds_helper(m, rel, ub=None) self.large_bounds_helper(m, rel, lb=None) - def oa_cuts_helper(self, m: _BlockData, rel: coramin.relaxations.BaseRelaxationData, rhs_expr: ExpressionBase, - num_pts: int = 30, supports_underestimator: bool = True, supports_overestimator: bool = True, - check_equal_at_points: bool = True): + def oa_cuts_helper( + self, + m: _BlockData, + rel: coramin.relaxations.BaseRelaxationData, + rhs_expr: ExpressionBase, + num_pts: int = 30, + supports_underestimator: bool = True, + supports_overestimator: bool = True, + check_equal_at_points: bool = True, + ): rhs_vars = rel.get_rhs_vars() sample_points = _grid_rhs_vars(rhs_vars, 5) for pt in sample_points: @@ -415,7 +616,9 @@ def oa_cuts_helper(self, m: _BlockData, rel: coramin.relaxations.BaseRelaxationD rel.rebuild() if rel.is_rhs_convex() or rel.is_rhs_concave(): self.assertEqual(len(rel._cuts), len(sample_points)) - self.valid_relaxation_helper(m, rel, rhs_expr, num_pts, supports_underestimator, supports_overestimator) + self.valid_relaxation_helper( + m, rel, rhs_expr, num_pts, supports_underestimator, supports_overestimator + ) if rel.is_rhs_convex(): check_under = True else: @@ -425,7 +628,9 @@ def oa_cuts_helper(self, m: _BlockData, rel: coramin.relaxations.BaseRelaxationD else: check_over = False if check_equal_at_points: - self.equal_at_points_helper(m, rel, rhs_expr, sample_points, check_under, check_over) + self.equal_at_points_helper( + m, rel, rhs_expr, sample_points, check_under, check_over + ) rel.push_oa_points('foo') rel.clear_oa_points() rel.rebuild() @@ -438,13 +643,22 @@ def oa_cuts_helper(self, m: _BlockData, rel: coramin.relaxations.BaseRelaxationD if rel.is_rhs_convex() or rel.is_rhs_concave(): self.assertEqual(len(rel._cuts), len(sample_points)) if check_equal_at_points: - self.equal_at_points_helper(m, rel, rhs_expr, sample_points, check_under, check_over) + self.equal_at_points_helper( + m, rel, rhs_expr, sample_points, check_under, check_over + ) rel.clear_oa_points() rel.rebuild() - def add_cuts_helper(self, m: _BlockData, rel: coramin.relaxations.BaseRelaxationData, rhs_expr: ExpressionBase, - num_pts: int = 30, supports_underestimator: bool = True, supports_overestimator: bool = True, - check_equal_at_points: bool = True): + def add_cuts_helper( + self, + m: _BlockData, + rel: coramin.relaxations.BaseRelaxationData, + rhs_expr: ExpressionBase, + num_pts: int = 30, + supports_underestimator: bool = True, + supports_overestimator: bool = True, + check_equal_at_points: bool = True, + ): rhs_vars = rel.get_rhs_vars() sample_points = _grid_rhs_vars(rhs_vars, 5) for keep_cut in [True, False]: @@ -454,19 +668,30 @@ def add_cuts_helper(self, m: _BlockData, rel: coramin.relaxations.BaseRelaxation v.value = p rel.get_aux_var().value = pe.value(rhs_expr) + offset rel.add_cut(keep_cut=keep_cut, check_violation=True) - self.valid_relaxation_helper(m, rel, rhs_expr, num_pts, supports_underestimator, supports_overestimator) + self.valid_relaxation_helper( + m, + rel, + rhs_expr, + num_pts, + supports_underestimator, + supports_overestimator, + ) if rel.has_convex_underestimator(): if offset < 0: self.assertEqual(len(rel._cuts), len(sample_points)) if check_equal_at_points: - self.equal_at_points_helper(m, rel, rhs_expr, sample_points, True, False) + self.equal_at_points_helper( + m, rel, rhs_expr, sample_points, True, False + ) else: self.assertEqual(len(rel._cuts), 2) if rel.has_concave_overestimator(): if offset > 0: self.assertEqual(len(rel._cuts), len(sample_points)) if check_equal_at_points: - self.equal_at_points_helper(m, rel, rhs_expr, sample_points, False, True) + self.equal_at_points_helper( + m, rel, rhs_expr, sample_points, False, True + ) else: self.assertEqual(len(rel._cuts), 2) if rel.has_convex_underestimator() or rel.has_concave_overestimator(): @@ -475,12 +700,18 @@ def add_cuts_helper(self, m: _BlockData, rel: coramin.relaxations.BaseRelaxation cuts_len = None rel.rebuild() if keep_cut: - if rel.has_convex_underestimator() or rel.has_concave_overestimator(): + if ( + rel.has_convex_underestimator() + or rel.has_concave_overestimator() + ): self.assertEqual(cuts_len, len(rel._cuts)) else: self.assertIsNone(rel._cuts) else: - if rel.has_convex_underestimator() or rel.has_concave_overestimator(): + if ( + rel.has_convex_underestimator() + or rel.has_concave_overestimator() + ): self.assertEqual(len(rel._cuts), 2) else: self.assertIsNone(rel._cuts) @@ -491,7 +722,9 @@ def add_cuts_helper(self, m: _BlockData, rel: coramin.relaxations.BaseRelaxation else: self.assertIsNone(rel._cuts) - def active_partition_helper(self, rel: coramin.relaxations.BasePWRelaxationData, partition_points): + def active_partition_helper( + self, rel: coramin.relaxations.BasePWRelaxationData, partition_points + ): rhs_var = rel.get_rhs_vars()[0] sample_points = _grid_rhs_vars([rhs_var], 30) partition_points.sort() @@ -515,7 +748,12 @@ def active_partition_helper(self, rel: coramin.relaxations.BasePWRelaxationData, self.assertAlmostEqual(active_lb, expected_lb) self.assertAlmostEqual(active_ub, expected_ub) - def pw_helper(self, m: _BlockData, rel: coramin.relaxations.BasePWRelaxationData, rhs_expr: ExpressionBase): + def pw_helper( + self, + m: _BlockData, + rel: coramin.relaxations.BasePWRelaxationData, + rhs_expr: ExpressionBase, + ): rhs_vars = rel.get_rhs_vars() sample_points = _grid_rhs_vars(rhs_vars, 5) part_points = list(set(i[0] for i in sample_points)) @@ -531,9 +769,16 @@ def pw_helper(self, m: _BlockData, rel: coramin.relaxations.BasePWRelaxationData rel.clear_partitions() rel.rebuild() - def util_methods_helper(self, rel: coramin.relaxations.BaseRelaxationData, rhs_expr: ExpressionBase, - aux_var: _GeneralVarData, expected_convex: bool, expected_concave: bool, - supports_underestimator: bool = True, supports_overestimator: bool = True): + def util_methods_helper( + self, + rel: coramin.relaxations.BaseRelaxationData, + rhs_expr: ExpressionBase, + aux_var: _GeneralVarData, + expected_convex: bool, + expected_concave: bool, + supports_underestimator: bool = True, + supports_overestimator: bool = True, + ): # test get_rhs_vars expected = ComponentSet(identify_variables(rhs_expr)) got = ComponentSet(rel.get_rhs_vars()) @@ -560,25 +805,36 @@ def util_methods_helper(self, rel: coramin.relaxations.BaseRelaxationData, rhs_e if supports_underestimator and supports_overestimator: out = io.StringIO() rel.pprint(ostream=out) - self.assertIn(f'{str(rel.get_aux_var())} == {str(rhs_expr)}', out.getvalue()) + self.assertIn( + f'{str(rel.get_aux_var())} == {str(rhs_expr)}', out.getvalue() + ) if supports_underestimator: rel.relaxation_side = coramin.RelaxationSide.UNDER rel.rebuild() out = io.StringIO() rel.pprint(ostream=out) - self.assertIn(f'{str(rel.get_aux_var())} >= {str(rhs_expr)}', out.getvalue()) + self.assertIn( + f'{str(rel.get_aux_var())} >= {str(rhs_expr)}', out.getvalue() + ) if supports_overestimator: rel.relaxation_side = coramin.RelaxationSide.OVER rel.rebuild() out = io.StringIO() rel.pprint(ostream=out) - self.assertIn(f'{str(rel.get_aux_var())} <= {str(rhs_expr)}', out.getvalue()) + self.assertIn( + f'{str(rel.get_aux_var())} <= {str(rhs_expr)}', out.getvalue() + ) rel.relaxation_side = original_relaxation_side rel.rebuild() - rel.pprint(verbose=True) # only checks that an error does not get raised... - - def deviation_helper(self, rel: coramin.relaxations.BaseRelaxationData, rhs_expr: ExpressionBase, - supports_underestimator: bool = True, supports_overestimator: bool = True): + rel.pprint(verbose=True) # only checks that an error does not get raised... + + def deviation_helper( + self, + rel: coramin.relaxations.BaseRelaxationData, + rhs_expr: ExpressionBase, + supports_underestimator: bool = True, + supports_overestimator: bool = True, + ): original_relaxation_side = rel.relaxation_side for v in rel.get_rhs_vars(): v.value = np.random.uniform(v.lb, v.ub) @@ -609,11 +865,20 @@ def deviation_helper(self, rel: coramin.relaxations.BaseRelaxationData, rhs_expr self.assertAlmostEqual(dev, 0) rel.relaxation_side = original_relaxation_side - def small_coef_helper(self, m: _BlockData, rel: coramin.relaxations.BaseRelaxationData, rhs_expr: ExpressionBase, - num_points: int = 30, check_underestimator: bool = True, check_overestimator: bool = True): + def small_coef_helper( + self, + m: _BlockData, + rel: coramin.relaxations.BaseRelaxationData, + rhs_expr: ExpressionBase, + num_points: int = 30, + check_underestimator: bool = True, + check_overestimator: bool = True, + ): rel.small_coef = 1e10 rel.rebuild() - self.valid_relaxation_helper(m, rel, rhs_expr, num_points, check_underestimator, check_overestimator) + self.valid_relaxation_helper( + m, rel, rhs_expr, num_points, check_underestimator, check_overestimator + ) rel.small_coef = 1e-10 rel.rebuild() @@ -638,7 +903,9 @@ def options_switching_helper(self, rel: coramin.relaxations.BaseRelaxationData): self.assertIsNotNone(rel._nonlinear) for v in rel.get_rhs_vars(): v.value = 1 - with self.assertRaisesRegex(ValueError, 'Can only add an OA cut when using a linear relaxation'): + with self.assertRaisesRegex( + ValueError, 'Can only add an OA cut when using a linear relaxation' + ): rel.add_cut(check_violation=False) rel.rebuild(build_nonlinear_constraint=True) self.assertIsNotNone(rel._original_constraint) @@ -682,7 +949,9 @@ def test_exp_relaxation(self): m = self.get_base_pyomo_model() m.rel = coramin.relaxations.PWUnivariateRelaxation() e = pe.exp(m.x) - m.rel.build(x=m.x, aux_var=m.z, shape=coramin.utils.FunctionShape.CONVEX, f_x_expr=e) + m.rel.build( + x=m.x, aux_var=m.z, shape=coramin.utils.FunctionShape.CONVEX, f_x_expr=e + ) self.options_switching_helper(m.rel) self.valid_relaxation_helper(m, m.rel, e) self.util_methods_helper(m.rel, e, m.z, True, False) @@ -703,7 +972,9 @@ def test_log_relaxation(self): m = self.get_base_pyomo_model(xlb=0.1, xub=2.5) m.rel = coramin.relaxations.PWUnivariateRelaxation() e = pe.log(m.x) - m.rel.build(x=m.x, aux_var=m.z, shape=coramin.utils.FunctionShape.CONCAVE, f_x_expr=e) + m.rel.build( + x=m.x, aux_var=m.z, shape=coramin.utils.FunctionShape.CONCAVE, f_x_expr=e + ) self.options_switching_helper(m.rel) self.valid_relaxation_helper(m, m.rel, e) self.util_methods_helper(m.rel, e, m.z, False, True) @@ -726,7 +997,9 @@ def test_univariate_convex_relaxation(self): m = self.get_base_pyomo_model(xlb=0.1, xub=2.5) m.rel = coramin.relaxations.PWUnivariateRelaxation() e = m.x * pe.log(m.x) - m.rel.build(x=m.x, aux_var=m.z, shape=coramin.utils.FunctionShape.CONVEX, f_x_expr=e) + m.rel.build( + x=m.x, aux_var=m.z, shape=coramin.utils.FunctionShape.CONVEX, f_x_expr=e + ) self.options_switching_helper(m.rel) self.valid_relaxation_helper(m, m.rel, e) self.util_methods_helper(m.rel, e, m.z, True, False) @@ -815,7 +1088,9 @@ def test_bilinear_relaxation(self): e = m.x * m.y self.valid_relaxation_helper(m, m.rel, e) self.util_methods_helper(m.rel, e, m.z, False, False) - self.equal_at_points_helper(m, m.rel, e, [(-1.5, -2), (0.8, 1), (-1.5, 1), (0.8, -2)]) + self.equal_at_points_helper( + m, m.rel, e, [(-1.5, -2), (0.8, 1), (-1.5, 1), (0.8, -2)] + ) self.oa_cuts_helper(m, m.rel, e) self.add_cuts_helper(m, m.rel, e) self.pw_helper(m, m.rel, e) @@ -824,7 +1099,10 @@ def test_bilinear_relaxation(self): self.large_bounds_helper(m, m.rel, lb=-1e6, ub=1e6) self.small_coef_helper(m, m.rel, e) self.original_constraint_helper(m, m.rel, e) - with self.assertRaisesRegex(ValueError, "Relaxations of type do not support relaxations that are not linear."): + with self.assertRaisesRegex( + ValueError, + "Relaxations of type do not support relaxations that are not linear.", + ): self.nonlinear_relaxation_helper(m, m.rel, e) self.relaxation_side_helper(m, m.rel, e, check_nonlinear_relaxation=False) self.deviation_helper(m.rel, e) @@ -832,7 +1110,11 @@ def test_bilinear_relaxation(self): def test_multivariate_convex(self): m = self.get_base_pyomo_model() m.rel = coramin.relaxations.MultivariateRelaxation() - m.rel.build(aux_var=m.z, shape=coramin.FunctionShape.CONVEX, f_x_expr=m.x**2 + m.y**2) + m.rel.build( + aux_var=m.z, + shape=coramin.FunctionShape.CONVEX, + f_x_expr=m.x**2 + m.y**2, + ) e = m.x**2 + m.y**2 self.options_switching_helper(m.rel) self.valid_relaxation_helper(m, m.rel, e, 10, True, False) @@ -841,7 +1123,9 @@ def test_multivariate_convex(self): m.rel.relaxation_side = coramin.RelaxationSide.OVER with self.assertRaises(ValueError): m.rel.relaxation_side = coramin.RelaxationSide.BOTH - self.equal_at_points_helper(m, m.rel, e, [(-1.5, -2), (0.8, 1)], True, False, True) + self.equal_at_points_helper( + m, m.rel, e, [(-1.5, -2), (0.8, 1)], True, False, True + ) self.oa_cuts_helper(m, m.rel, e, 30, True, False) self.add_cuts_helper(m, m.rel, e, 30, True, False) self.changing_bounds_helper(m, m.rel, e, 5, True, False) @@ -855,7 +1139,11 @@ def test_multivariate_convex(self): def test_multivariate_concave(self): m = self.get_base_pyomo_model() m.rel = coramin.relaxations.MultivariateRelaxation() - m.rel.build(aux_var=m.z, shape=coramin.FunctionShape.CONCAVE, f_x_expr=-m.x**2 - m.y**2) + m.rel.build( + aux_var=m.z, + shape=coramin.FunctionShape.CONCAVE, + f_x_expr=-m.x**2 - m.y**2, + ) e = -m.x**2 - m.y**2 self.valid_relaxation_helper(m, m.rel, e, 10, False, True) self.util_methods_helper(m.rel, e, m.z, False, True, False, True) @@ -863,7 +1151,9 @@ def test_multivariate_concave(self): m.rel.relaxation_side = coramin.RelaxationSide.UNDER with self.assertRaises(ValueError): m.rel.relaxation_side = coramin.RelaxationSide.BOTH - self.equal_at_points_helper(m, m.rel, e, [(-1.5, -2), (0.8, 1)], False, True, True) + self.equal_at_points_helper( + m, m.rel, e, [(-1.5, -2), (0.8, 1)], False, True, True + ) self.oa_cuts_helper(m, m.rel, e, 30, False, True) self.add_cuts_helper(m, m.rel, e, 30, False, True) self.changing_bounds_helper(m, m.rel, e, 5, False, True) @@ -878,10 +1168,12 @@ def test_alpha_bb1(self): m = self.get_base_pyomo_model() m.rel = coramin.relaxations.AlphaBBRelaxation() m.rel.build( - aux_var=m.z, f_x_expr=m.x*m.y, relaxation_side=coramin.RelaxationSide.UNDER, + aux_var=m.z, + f_x_expr=m.x * m.y, + relaxation_side=coramin.RelaxationSide.UNDER, eigenvalue_opt=appsi.solvers.Gurobi(), ) - e = m.x*m.y + e = m.x * m.y self.options_switching_helper(m.rel) self.valid_relaxation_helper(m, m.rel, e, 10, True, False) self.util_methods_helper(m.rel, e, m.z, False, False, True, False) @@ -889,7 +1181,9 @@ def test_alpha_bb1(self): m.rel.relaxation_side = coramin.RelaxationSide.OVER with self.assertRaises(ValueError): m.rel.relaxation_side = coramin.RelaxationSide.BOTH - self.equal_at_points_helper(m, m.rel, e, [(-1.5, -2), (0.8, 1)], True, False, True) + self.equal_at_points_helper( + m, m.rel, e, [(-1.5, -2), (0.8, 1)], True, False, True + ) self.oa_cuts_helper(m, m.rel, e, 30, True, False, False) self.add_cuts_helper(m, m.rel, e, 30, True, False, False) self.infinite_bounds_helper(m, m.rel) @@ -902,7 +1196,8 @@ def test_alpha_bb2(self): m = self.get_base_pyomo_model() m.rel = coramin.relaxations.AlphaBBRelaxation() m.rel.build( - aux_var=m.z, f_x_expr=-m.x**2 - m.y**2, + aux_var=m.z, + f_x_expr=-m.x**2 - m.y**2, relaxation_side=coramin.RelaxationSide.UNDER, eigenvalue_opt=appsi.solvers.Gurobi(), ) @@ -913,7 +1208,9 @@ def test_alpha_bb2(self): m.rel.relaxation_side = coramin.RelaxationSide.OVER with self.assertRaises(ValueError): m.rel.relaxation_side = coramin.RelaxationSide.BOTH - self.equal_at_points_helper(m, m.rel, e, [(-1.5, -2), (0.8, 1)], True, False, True) + self.equal_at_points_helper( + m, m.rel, e, [(-1.5, -2), (0.8, 1)], True, False, True + ) self.oa_cuts_helper(m, m.rel, e, 30, True, False, False) self.add_cuts_helper(m, m.rel, e, 30, True, False, False) self.changing_bounds_helper(m, m.rel, e, 5, True, False, False) @@ -928,7 +1225,8 @@ def test_alpha_bb3(self): m = self.get_base_pyomo_model() m.rel = coramin.relaxations.AlphaBBRelaxation() m.rel.build( - aux_var=m.z, f_x_expr=m.x**2 + m.y**2, + aux_var=m.z, + f_x_expr=m.x**2 + m.y**2, relaxation_side=coramin.RelaxationSide.UNDER, eigenvalue_opt=appsi.solvers.Gurobi(), ) @@ -939,7 +1237,9 @@ def test_alpha_bb3(self): m.rel.relaxation_side = coramin.RelaxationSide.OVER with self.assertRaises(ValueError): m.rel.relaxation_side = coramin.RelaxationSide.BOTH - self.equal_at_points_helper(m, m.rel, e, [(-1.5, -2), (0.8, 1)], True, False, True) + self.equal_at_points_helper( + m, m.rel, e, [(-1.5, -2), (0.8, 1)], True, False, True + ) self.oa_cuts_helper(m, m.rel, e, 30, True, False, True) self.add_cuts_helper(m, m.rel, e, 30, True, False, True) self.changing_bounds_helper(m, m.rel, e, 5, True, False, True) diff --git a/pyomo/contrib/coramin/relaxations/tests/test_relaxations_base.py b/pyomo/contrib/coramin/relaxations/tests/test_relaxations_base.py index c39b5fb9650..994ec502055 100644 --- a/pyomo/contrib/coramin/relaxations/tests/test_relaxations_base.py +++ b/pyomo/contrib/coramin/relaxations/tests/test_relaxations_base.py @@ -92,14 +92,18 @@ def test_push_oa_points_with_key(self): self.assertEqual(list(m.c._oa_points.keys()), [(-1,), (1,), (0,), (0.5,)]) m.c.push_oa_points() m.c.add_oa_point(pe.ComponentMap([(m.x, -0.5)])) - self.assertEqual(list(m.c._oa_points.keys()), [(-1,), (1,), (0,), (0.5,), (-0.5,)]) + self.assertEqual( + list(m.c._oa_points.keys()), [(-1,), (1,), (0,), (0.5,), (-0.5,)] + ) m.c.push_oa_points(key='second key') m.c.pop_oa_points(key='first key') self.assertEqual(list(m.c._oa_points.keys()), [(-1,), (1,), (0,)]) m.c.pop_oa_points() self.assertEqual(list(m.c._oa_points.keys()), [(-1,), (1,), (0,), (0.5,)]) m.c.pop_oa_points(key='second key') - self.assertEqual(list(m.c._oa_points.keys()), [(-1,), (1,), (0,), (0.5,), (-0.5,)]) + self.assertEqual( + list(m.c._oa_points.keys()), [(-1,), (1,), (0,), (0.5,), (-0.5,)] + ) def test_push_and_pop_partitions(self): m = pe.ConcreteModel() diff --git a/pyomo/contrib/coramin/relaxations/tests/test_univariate_relaxations.py b/pyomo/contrib/coramin/relaxations/tests/test_univariate_relaxations.py index 7a69817a278..b2ed7875fb5 100644 --- a/pyomo/contrib/coramin/relaxations/tests/test_univariate_relaxations.py +++ b/pyomo/contrib/coramin/relaxations/tests/test_univariate_relaxations.py @@ -16,8 +16,14 @@ def setUpClass(cls): model.obj = pe.Objective(expr=model.y, sense=pe.maximize) model.pw_exp = coramin.relaxations.PWUnivariateRelaxation() - model.pw_exp.build(x=model.x, aux_var=model.y, pw_repn='INC', shape=coramin.utils.FunctionShape.CONVEX, - relaxation_side=coramin.utils.RelaxationSide.BOTH, f_x_expr=pe.exp(model.x)) + model.pw_exp.build( + x=model.x, + aux_var=model.y, + pw_repn='INC', + shape=coramin.utils.FunctionShape.CONVEX, + relaxation_side=coramin.utils.RelaxationSide.BOTH, + f_x_expr=pe.exp(model.x), + ) model.pw_exp.add_partition_point(-0.5) model.pw_exp.add_partition_point(0.5) model.pw_exp.rebuild() @@ -51,7 +57,14 @@ def test_exp_lb(self): class TestUnivariate(unittest.TestCase): - def helper(self, func, shape, bounds_list, relaxation_class, relaxation_side=coramin.utils.RelaxationSide.BOTH): + def helper( + self, + func, + shape, + bounds_list, + relaxation_class, + relaxation_side=coramin.utils.RelaxationSide.BOTH, + ): for lb, ub in bounds_list: num_segments_list = [1, 2, 3] m = pe.ConcreteModel() @@ -59,11 +72,13 @@ def helper(self, func, shape, bounds_list, relaxation_class, relaxation_side=cor m.aux = pe.Var() if relaxation_class is coramin.relaxations.PWUnivariateRelaxation: m.c = coramin.relaxations.PWUnivariateRelaxation() - m.c.build(x=m.x, - aux_var=m.aux, - relaxation_side=relaxation_side, - shape=shape, - f_x_expr=func(m.x)) + m.c.build( + x=m.x, + aux_var=m.aux, + relaxation_side=relaxation_side, + shape=shape, + f_x_expr=func(m.x), + ) else: m.c = relaxation_class() m.c.build(x=m.x, aux_var=m.aux, relaxation_side=relaxation_side) @@ -84,58 +99,111 @@ def helper(self, func, shape, bounds_list, relaxation_class, relaxation_side=cor m.p.value = _x opt.remove_constraint(m.c2) opt.add_constraint(m.c2) - if relaxation_side in {coramin.utils.RelaxationSide.BOTH, coramin.utils.RelaxationSide.UNDER}: + if relaxation_side in { + coramin.utils.RelaxationSide.BOTH, + coramin.utils.RelaxationSide.UNDER, + }: m.obj = pe.Objective(expr=m.aux) opt.set_objective(m.obj) res = opt.solve() - self.assertEqual(res.solver.termination_condition, pe.TerminationCondition.optimal) + self.assertEqual( + res.solver.termination_condition, + pe.TerminationCondition.optimal, + ) self.assertLessEqual(m.aux.value, func(_x) + 1e-10) del m.obj - if relaxation_side in {coramin.utils.RelaxationSide.BOTH, coramin.utils.RelaxationSide.OVER}: + if relaxation_side in { + coramin.utils.RelaxationSide.BOTH, + coramin.utils.RelaxationSide.OVER, + }: m.obj = pe.Objective(expr=m.aux, sense=pe.maximize) opt.set_objective(m.obj) res = opt.solve() - self.assertEqual(res.solver.termination_condition, pe.TerminationCondition.optimal) + self.assertEqual( + res.solver.termination_condition, + pe.TerminationCondition.optimal, + ) self.assertGreaterEqual(m.aux.value, func(_x) - 1e-10) del m.obj def test_exp(self): - self.helper(func=pe.exp, shape=coramin.utils.FunctionShape.CONVEX, bounds_list=[(-1, 1)], - relaxation_class=coramin.relaxations.PWUnivariateRelaxation) + self.helper( + func=pe.exp, + shape=coramin.utils.FunctionShape.CONVEX, + bounds_list=[(-1, 1)], + relaxation_class=coramin.relaxations.PWUnivariateRelaxation, + ) def test_log(self): - self.helper(func=pe.log, shape=coramin.utils.FunctionShape.CONCAVE, bounds_list=[(0.5, 1.5)], - relaxation_class=coramin.relaxations.PWUnivariateRelaxation) + self.helper( + func=pe.log, + shape=coramin.utils.FunctionShape.CONCAVE, + bounds_list=[(0.5, 1.5)], + relaxation_class=coramin.relaxations.PWUnivariateRelaxation, + ) def test_quadratic(self): def quadratic_func(x): return x**2 - self.helper(func=quadratic_func, shape=None, bounds_list=[(-1, 2)], - relaxation_class=coramin.relaxations.PWXSquaredRelaxation) + + self.helper( + func=quadratic_func, + shape=None, + bounds_list=[(-1, 2)], + relaxation_class=coramin.relaxations.PWXSquaredRelaxation, + ) def test_arctan(self): - self.helper(func=pe.atan, shape=None, bounds_list=[(-1, 1), (-1, 0), (0, 1)], - relaxation_class=coramin.relaxations.PWArctanRelaxation) - self.helper(func=pe.atan, shape=None, bounds_list=[(-0.1, 1)], - relaxation_class=coramin.relaxations.PWArctanRelaxation, - relaxation_side=coramin.utils.RelaxationSide.OVER) - self.helper(func=pe.atan, shape=None, bounds_list=[(-1, 0.1)], - relaxation_class=coramin.relaxations.PWArctanRelaxation, - relaxation_side=coramin.utils.RelaxationSide.UNDER) + self.helper( + func=pe.atan, + shape=None, + bounds_list=[(-1, 1), (-1, 0), (0, 1)], + relaxation_class=coramin.relaxations.PWArctanRelaxation, + ) + self.helper( + func=pe.atan, + shape=None, + bounds_list=[(-0.1, 1)], + relaxation_class=coramin.relaxations.PWArctanRelaxation, + relaxation_side=coramin.utils.RelaxationSide.OVER, + ) + self.helper( + func=pe.atan, + shape=None, + bounds_list=[(-1, 0.1)], + relaxation_class=coramin.relaxations.PWArctanRelaxation, + relaxation_side=coramin.utils.RelaxationSide.UNDER, + ) def test_sin(self): - self.helper(func=pe.sin, shape=None, bounds_list=[(-1, 1), (-1, 0), (0, 1)], - relaxation_class=coramin.relaxations.PWSinRelaxation) - self.helper(func=pe.sin, shape=None, bounds_list=[(-0.1, 1)], - relaxation_class=coramin.relaxations.PWSinRelaxation, - relaxation_side=coramin.utils.RelaxationSide.OVER) - self.helper(func=pe.sin, shape=None, bounds_list=[(-1, 0.1)], - relaxation_class=coramin.relaxations.PWSinRelaxation, - relaxation_side=coramin.utils.RelaxationSide.UNDER) + self.helper( + func=pe.sin, + shape=None, + bounds_list=[(-1, 1), (-1, 0), (0, 1)], + relaxation_class=coramin.relaxations.PWSinRelaxation, + ) + self.helper( + func=pe.sin, + shape=None, + bounds_list=[(-0.1, 1)], + relaxation_class=coramin.relaxations.PWSinRelaxation, + relaxation_side=coramin.utils.RelaxationSide.OVER, + ) + self.helper( + func=pe.sin, + shape=None, + bounds_list=[(-1, 0.1)], + relaxation_class=coramin.relaxations.PWSinRelaxation, + relaxation_side=coramin.utils.RelaxationSide.UNDER, + ) def test_cos(self): - self.helper(func=pe.cos, shape=None, bounds_list=[(-1, 1)], - relaxation_class=coramin.relaxations.PWCosRelaxation) + self.helper( + func=pe.cos, + shape=None, + bounds_list=[(-1, 1)], + relaxation_class=coramin.relaxations.PWCosRelaxation, + ) class TestFeasibility(unittest.TestCase): @@ -146,8 +214,13 @@ def test_univariate_exp(self): m.y = pe.Var() m.z = pe.Var(bounds=(0, None)) m.c = coramin.relaxations.PWUnivariateRelaxation() - m.c.build(x=m.x, aux_var=m.y, relaxation_side=coramin.utils.RelaxationSide.BOTH, - shape=coramin.utils.FunctionShape.CONVEX, f_x_expr=pe.exp(m.x)) + m.c.build( + x=m.x, + aux_var=m.y, + relaxation_side=coramin.utils.RelaxationSide.BOTH, + shape=coramin.utils.FunctionShape.CONVEX, + f_x_expr=pe.exp(m.x), + ) m.c.rebuild() m.c2 = pe.ConstraintList() m.c2.add(m.z >= m.y - m.p) @@ -159,7 +232,9 @@ def test_univariate_exp(self): m.x.fix(xval) m.p.value = pval res = opt.solve(m, tee=False) - self.assertTrue(res.solver.termination_condition == pe.TerminationCondition.optimal) + self.assertTrue( + res.solver.termination_condition == pe.TerminationCondition.optimal + ) self.assertAlmostEqual(m.y.value, m.p.value, 6) def test_pw_exp(self): @@ -169,8 +244,13 @@ def test_pw_exp(self): m.y = pe.Var() m.z = pe.Var(bounds=(0, None)) m.c = coramin.relaxations.PWUnivariateRelaxation() - m.c.build(x=m.x, aux_var=m.y, relaxation_side=coramin.utils.RelaxationSide.BOTH, - shape=coramin.utils.FunctionShape.CONVEX, f_x_expr=pe.exp(m.x)) + m.c.build( + x=m.x, + aux_var=m.y, + relaxation_side=coramin.utils.RelaxationSide.BOTH, + shape=coramin.utils.FunctionShape.CONVEX, + f_x_expr=pe.exp(m.x), + ) m.c.add_partition_point(-0.25) m.c.add_partition_point(0.25) m.c.rebuild() @@ -184,7 +264,9 @@ def test_pw_exp(self): m.x.fix(xval) m.p.value = pval res = opt.solve(m, tee=False) - self.assertTrue(res.solver.termination_condition == pe.TerminationCondition.optimal) + self.assertTrue( + res.solver.termination_condition == pe.TerminationCondition.optimal + ) self.assertAlmostEqual(m.y.value, m.p.value, 6) def test_univariate_log(self): @@ -194,8 +276,13 @@ def test_univariate_log(self): m.y = pe.Var() m.z = pe.Var(bounds=(0, None)) m.c = coramin.relaxations.PWUnivariateRelaxation() - m.c.build(x=m.x, aux_var=m.y, relaxation_side=coramin.utils.RelaxationSide.BOTH, - shape=coramin.utils.FunctionShape.CONCAVE, f_x_expr=pe.log(m.x)) + m.c.build( + x=m.x, + aux_var=m.y, + relaxation_side=coramin.utils.RelaxationSide.BOTH, + shape=coramin.utils.FunctionShape.CONCAVE, + f_x_expr=pe.log(m.x), + ) m.c.rebuild() m.c2 = pe.ConstraintList() m.c2.add(m.z >= m.y - m.p) @@ -207,7 +294,9 @@ def test_univariate_log(self): m.x.fix(xval) m.p.value = pval res = opt.solve(m, tee=False) - self.assertTrue(res.solver.termination_condition == pe.TerminationCondition.optimal) + self.assertTrue( + res.solver.termination_condition == pe.TerminationCondition.optimal + ) self.assertAlmostEqual(m.y.value, m.p.value, 6) def test_pw_log(self): @@ -217,8 +306,13 @@ def test_pw_log(self): m.y = pe.Var() m.z = pe.Var(bounds=(0, None)) m.c = coramin.relaxations.PWUnivariateRelaxation() - m.c.build(x=m.x, aux_var=m.y, relaxation_side=coramin.utils.RelaxationSide.BOTH, - shape=coramin.utils.FunctionShape.CONCAVE, f_x_expr=pe.log(m.x)) + m.c.build( + x=m.x, + aux_var=m.y, + relaxation_side=coramin.utils.RelaxationSide.BOTH, + shape=coramin.utils.FunctionShape.CONCAVE, + f_x_expr=pe.log(m.x), + ) m.c.add_partition_point(0.9) m.c.add_partition_point(1.1) m.c.rebuild() @@ -232,7 +326,9 @@ def test_pw_log(self): m.x.fix(xval) m.p.value = pval res = opt.solve(m, tee=False) - self.assertTrue(res.solver.termination_condition == pe.TerminationCondition.optimal) + self.assertTrue( + res.solver.termination_condition == pe.TerminationCondition.optimal + ) self.assertAlmostEqual(m.y.value, m.p.value, 6) def test_x_fixed(self): @@ -242,8 +338,13 @@ def test_x_fixed(self): m.x.setlb(0) m.x.setub(0) m.c = coramin.relaxations.PWUnivariateRelaxation() - m.c.build(x=m.x, aux_var=m.y, relaxation_side=coramin.utils.RelaxationSide.BOTH, - shape=coramin.utils.FunctionShape.CONVEX, f_x_expr=pe.exp(m.x)) + m.c.build( + x=m.x, + aux_var=m.y, + relaxation_side=coramin.utils.RelaxationSide.BOTH, + shape=coramin.utils.FunctionShape.CONVEX, + f_x_expr=pe.exp(m.x), + ) m.obj = pe.Objective(expr=m.y) opt = pe.SolverFactory('appsi_gurobi') res = opt.solve(m) @@ -270,5 +371,7 @@ def test_x_sq(self): m.x.fix(xval) m.p.value = pval res = opt.solve(m, tee=False) - self.assertTrue(res.solver.termination_condition == pe.TerminationCondition.optimal) + self.assertTrue( + res.solver.termination_condition == pe.TerminationCondition.optimal + ) self.assertAlmostEqual(m.y.value, m.p.value, 6) diff --git a/pyomo/contrib/coramin/relaxations/univariate.py b/pyomo/contrib/coramin/relaxations/univariate.py index 1bef9a7b132..09839640459 100644 --- a/pyomo/contrib/coramin/relaxations/univariate.py +++ b/pyomo/contrib/coramin/relaxations/univariate.py @@ -12,6 +12,7 @@ import logging from typing import Optional, Union, Sequence from pyomo.core.expr.calculus.diff_with_pyomo import reverse_sd + logger = logging.getLogger(__name__) pe = pyo @@ -26,28 +27,44 @@ def _sin_underestimator_fn(x, UB): def _compute_sine_overestimator_tangent_point(vlb): assert vlb < 0 - tangent_point, res = scipy.optimize.bisect(f=_sin_overestimator_fn, a=0, b=math.pi / 2, args=(vlb,), - full_output=True, disp=False) + tangent_point, res = scipy.optimize.bisect( + f=_sin_overestimator_fn, + a=0, + b=math.pi / 2, + args=(vlb,), + full_output=True, + disp=False, + ) if res.converged: tangent_point = float(tangent_point) slope = float(np.cos(tangent_point)) intercept = float(np.sin(vlb) - slope * vlb) return tangent_point, slope, intercept else: - raise RuntimeError('Unable to build relaxation for sin(x)\nBisect info: ' + str(res)) + raise RuntimeError( + 'Unable to build relaxation for sin(x)\nBisect info: ' + str(res) + ) def _compute_sine_underestimator_tangent_point(vub): assert vub > 0 - tangent_point, res = scipy.optimize.bisect(f=_sin_underestimator_fn, a=-math.pi / 2, b=0, args=(vub,), - full_output=True, disp=False) + tangent_point, res = scipy.optimize.bisect( + f=_sin_underestimator_fn, + a=-math.pi / 2, + b=0, + args=(vub,), + full_output=True, + disp=False, + ) if res.converged: tangent_point = float(tangent_point) slope = float(np.cos(-tangent_point)) intercept = float(np.sin(vub) - slope * vub) return tangent_point, slope, intercept else: - raise RuntimeError('Unable to build relaxation for sin(x)\nBisect info: ' + str(res)) + raise RuntimeError( + 'Unable to build relaxation for sin(x)\nBisect info: ' + str(res) + ) def _atan_overestimator_fn(x, LB): @@ -60,28 +77,44 @@ def _atan_underestimator_fn(x, UB): def _compute_arctan_overestimator_tangent_point(vlb): assert vlb < 0 - tangent_point, res = scipy.optimize.bisect(f=_atan_overestimator_fn, a=0, b=abs(vlb), args=(vlb,), - full_output=True, disp=False) + tangent_point, res = scipy.optimize.bisect( + f=_atan_overestimator_fn, + a=0, + b=abs(vlb), + args=(vlb,), + full_output=True, + disp=False, + ) if res.converged: tangent_point = float(tangent_point) - slope = 1/(1 + tangent_point**2) + slope = 1 / (1 + tangent_point**2) intercept = float(np.arctan(vlb) - slope * vlb) return tangent_point, slope, intercept else: - raise RuntimeError('Unable to build relaxation for arctan(x)\nBisect info: ' + str(res)) + raise RuntimeError( + 'Unable to build relaxation for arctan(x)\nBisect info: ' + str(res) + ) def _compute_arctan_underestimator_tangent_point(vub): assert vub > 0 - tangent_point, res = scipy.optimize.bisect(f=_atan_underestimator_fn, a=-vub, b=0, args=(vub,), - full_output=True, disp=False) + tangent_point, res = scipy.optimize.bisect( + f=_atan_underestimator_fn, + a=-vub, + b=0, + args=(vub,), + full_output=True, + disp=False, + ) if res.converged: tangent_point = float(tangent_point) - slope = 1/(1 + tangent_point**2) + slope = 1 / (1 + tangent_point**2) intercept = float(np.arctan(vub) - slope * vub) return tangent_point, slope, intercept else: - raise RuntimeError('Unable to build relaxation for arctan(x)\nBisect info: ' + str(res)) + raise RuntimeError( + 'Unable to build relaxation for arctan(x)\nBisect info: ' + str(res) + ) class _FxExpr(object): @@ -113,12 +146,22 @@ def __call__(self, _xval): def _func_wrapper(obj): def _func(m, val): return obj(val) + return _func -def _pw_univariate_relaxation(b, x, w, x_pts, f_x_expr, pw_repn='INC', shape=FunctionShape.UNKNOWN, - relaxation_side=RelaxationSide.BOTH, large_eval_tol=math.inf, - safety_tol=0): +def _pw_univariate_relaxation( + b, + x, + w, + x_pts, + f_x_expr, + pw_repn='INC', + shape=FunctionShape.UNKNOWN, + relaxation_side=RelaxationSide.BOTH, + large_eval_tol=math.inf, + safety_tol=0, +): """ This function creates piecewise envelopes to relax "w=f(x)" where f(x) is univariate and either convex over the entire domain of x or concave over the entire domain of x. @@ -144,7 +187,7 @@ def _pw_univariate_relaxation(b, x, w, x_pts, f_x_expr, pw_repn='INC', shape=Fun relaxation_side: RelaxationSide Provide the desired side for the relaxation (OVER, UNDER, or BOTH) large_eval_tol: float - To avoid numerical problems, if f_x_expr or its derivative evaluates to a value larger than large_eval_tol, + To avoid numerical problems, if f_x_expr or its derivative evaluates to a value larger than large_eval_tol, at a point in x_pts, then that point is skipped. """ assert shape in {FunctionShape.CONCAVE, FunctionShape.CONVEX} @@ -168,10 +211,16 @@ def _pw_univariate_relaxation(b, x, w, x_pts, f_x_expr, pw_repn='INC', shape=Fun # Do the non-convex piecewise portion if shape=CONCAVE and relaxation_side=Under/BOTH # or if shape=CONVEX and relaxation_side=Over/BOTH pw_constr_type = None - if shape == FunctionShape.CONVEX and relaxation_side in {RelaxationSide.OVER, RelaxationSide.BOTH}: + if shape == FunctionShape.CONVEX and relaxation_side in { + RelaxationSide.OVER, + RelaxationSide.BOTH, + }: pw_constr_type = 'UB' _eval = _FxExpr(expr=f_x_expr + safety_tol, x=x) - if shape == FunctionShape.CONCAVE and relaxation_side in {RelaxationSide.UNDER, RelaxationSide.BOTH}: + if shape == FunctionShape.CONCAVE and relaxation_side in { + RelaxationSide.UNDER, + RelaxationSide.BOTH, + }: pw_constr_type = 'LB' _eval = _FxExpr(expr=f_x_expr - safety_tol, x=x) @@ -183,22 +232,32 @@ def _pw_univariate_relaxation(b, x, w, x_pts, f_x_expr, pw_repn='INC', shape=Fun try: f = _eval(_pt) if abs(f) >= large_eval_tol: - logger.warning(f'Skipping pt {_pt} for var {str(x)} because |{str(f_x_expr)}| ' - f'evaluated at {_pt} is larger than {large_eval_tol}') + logger.warning( + f'Skipping pt {_pt} for var {str(x)} because |{str(f_x_expr)}| ' + f'evaluated at {_pt} is larger than {large_eval_tol}' + ) continue tmp_pts.append(_pt) except (ZeroDivisionError, ValueError, OverflowError): pass - if len(tmp_pts) >= 2 and tmp_pts[0] == x_pts[0] and tmp_pts[-1] == x_pts[-1]: - b.pw_linear_under_over = pyo.Piecewise(w, x, - pw_pts=tmp_pts, - pw_repn=pw_repn, - pw_constr_type=pw_constr_type, - f_rule=_func_wrapper(_eval) - ) - - -def pw_sin_relaxation(b, x, w, x_pts, relaxation_side=RelaxationSide.BOTH, safety_tol=1e-10): + if ( + len(tmp_pts) >= 2 + and tmp_pts[0] == x_pts[0] + and tmp_pts[-1] == x_pts[-1] + ): + b.pw_linear_under_over = pyo.Piecewise( + w, + x, + pw_pts=tmp_pts, + pw_repn=pw_repn, + pw_constr_type=pw_constr_type, + f_rule=_func_wrapper(_eval), + ) + + +def pw_sin_relaxation( + b, x, w, x_pts, relaxation_side=RelaxationSide.BOTH, safety_tol=1e-10 +): """ This function creates piecewise relaxations to relax "w=sin(x)" for -pi/2 <= x <= pi/2. @@ -237,8 +296,16 @@ def pw_sin_relaxation(b, x, w, x_pts, relaxation_side=RelaxationSide.BOTH, safet if xub > np.pi / 2.0: return - OE_tangent_x, OE_tangent_slope, OE_tangent_intercept = _compute_sine_overestimator_tangent_point(xlb) - UE_tangent_x, UE_tangent_slope, UE_tangent_intercept = _compute_sine_underestimator_tangent_point(xub) + ( + OE_tangent_x, + OE_tangent_slope, + OE_tangent_intercept, + ) = _compute_sine_overestimator_tangent_point(xlb) + ( + UE_tangent_x, + UE_tangent_slope, + UE_tangent_intercept, + ) = _compute_sine_underestimator_tangent_point(xub) non_piecewise_overestimators_pts = [] non_piecewise_underestimator_pts = [] @@ -247,7 +314,9 @@ def pw_sin_relaxation(b, x, w, x_pts, relaxation_side=RelaxationSide.BOTH, safet new_x_pts = [i for i in x_pts if i < OE_tangent_x] new_x_pts.append(xub) non_piecewise_overestimators_pts = [OE_tangent_x] - non_piecewise_overestimators_pts.extend(i for i in x_pts if i > OE_tangent_x) + non_piecewise_overestimators_pts.extend( + i for i in x_pts if i > OE_tangent_x + ) x_pts = new_x_pts elif relaxation_side == RelaxationSide.UNDER: if UE_tangent_x > xlb: @@ -260,13 +329,17 @@ def pw_sin_relaxation(b, x, w, x_pts, relaxation_side=RelaxationSide.BOTH, safet b.non_piecewise_overestimators = pyo.ConstraintList() b.non_piecewise_underestimators = pyo.ConstraintList() for pt in non_piecewise_overestimators_pts: - b.non_piecewise_overestimators.add(w <= math.sin(pt) + safety_tol + (x - pt) * math.cos(pt)) + b.non_piecewise_overestimators.add( + w <= math.sin(pt) + safety_tol + (x - pt) * math.cos(pt) + ) for pt in non_piecewise_underestimator_pts: - b.non_piecewise_underestimators.add(w >= math.sin(pt) - safety_tol + (x - pt) * math.cos(pt)) + b.non_piecewise_underestimators.add( + w >= math.sin(pt) - safety_tol + (x - pt) * math.cos(pt) + ) intervals = [] - for i in range(len(x_pts)-1): - intervals.append((x_pts[i], x_pts[i+1])) + for i in range(len(x_pts) - 1): + intervals.append((x_pts[i], x_pts[i + 1])) b.interval_set = pyo.Set(initialize=range(len(intervals)), ordered=True) b.x = pyo.Var(b.interval_set) @@ -296,49 +369,93 @@ def pw_sin_relaxation(b, x, w, x_pts, relaxation_side=RelaxationSide.BOTH, safet if x0 < 0 and x1 <= 0: slope = (math.sin(x1) - math.sin(x0)) / (x1 - x0) intercept = math.sin(x0) - slope * x0 - b.overestimators.add(b.w[i] <= slope * b.x[i] + (intercept + safety_tol) * b.lam[i]) + b.overestimators.add( + b.w[i] <= slope * b.x[i] + (intercept + safety_tol) * b.lam[i] + ) elif (x0 < 0) and (x1 > 0): - tangent_x, tangent_slope, tangent_intercept = _compute_sine_overestimator_tangent_point(x0) + ( + tangent_x, + tangent_slope, + tangent_intercept, + ) = _compute_sine_overestimator_tangent_point(x0) if tangent_x <= x1: - b.overestimators.add(b.w[i] <= tangent_slope * b.x[i] + (tangent_intercept + safety_tol) * b.lam[i]) - b.overestimators.add(b.w[i] <= math.cos(x1) * b.x[i] + - (math.sin(x1) - x1 * math.cos(x1) + safety_tol) * b.lam[i]) + b.overestimators.add( + b.w[i] + <= tangent_slope * b.x[i] + + (tangent_intercept + safety_tol) * b.lam[i] + ) + b.overestimators.add( + b.w[i] + <= math.cos(x1) * b.x[i] + + (math.sin(x1) - x1 * math.cos(x1) + safety_tol) * b.lam[i] + ) else: slope = (math.sin(x1) - math.sin(x0)) / (x1 - x0) intercept = math.sin(x0) - slope * x0 - b.overestimators.add(b.w[i] <= slope * b.x[i] + (intercept + safety_tol) * b.lam[i]) + b.overestimators.add( + b.w[i] <= slope * b.x[i] + (intercept + safety_tol) * b.lam[i] + ) else: - b.overestimators.add(b.w[i] <= math.cos(x0)*b.x[i] + - (math.sin(x0) - x0*math.cos(x0) + safety_tol)*b.lam[i]) - b.overestimators.add(b.w[i] <= math.cos(x1)*b.x[i] + - (math.sin(x1) - x1*math.cos(x1) + safety_tol)*b.lam[i]) + b.overestimators.add( + b.w[i] + <= math.cos(x0) * b.x[i] + + (math.sin(x0) - x0 * math.cos(x0) + safety_tol) * b.lam[i] + ) + b.overestimators.add( + b.w[i] + <= math.cos(x1) * b.x[i] + + (math.sin(x1) - x1 * math.cos(x1) + safety_tol) * b.lam[i] + ) # Underestimators if relaxation_side in {RelaxationSide.UNDER, RelaxationSide.BOTH}: if x0 >= 0 and x1 > 0: slope = (math.sin(x1) - math.sin(x0)) / (x1 - x0) intercept = math.sin(x0) - slope * x0 - b.underestimators.add(b.w[i] >= slope * b.x[i] + (intercept - safety_tol) * b.lam[i]) + b.underestimators.add( + b.w[i] >= slope * b.x[i] + (intercept - safety_tol) * b.lam[i] + ) elif (x1 > 0) and (x0 < 0): - tangent_x, tangent_slope, tangent_intercept = _compute_sine_underestimator_tangent_point(x1) + ( + tangent_x, + tangent_slope, + tangent_intercept, + ) = _compute_sine_underestimator_tangent_point(x1) if tangent_x >= x0: - b.underestimators.add(b.w[i] >= tangent_slope*b.x[i] + (tangent_intercept - safety_tol)*b.lam[i]) - b.underestimators.add(b.w[i] >= math.cos(x0)*b.x[i] + - (math.sin(x0) - x0 * math.cos(x0) - safety_tol)*b.lam[i]) + b.underestimators.add( + b.w[i] + >= tangent_slope * b.x[i] + + (tangent_intercept - safety_tol) * b.lam[i] + ) + b.underestimators.add( + b.w[i] + >= math.cos(x0) * b.x[i] + + (math.sin(x0) - x0 * math.cos(x0) - safety_tol) * b.lam[i] + ) else: slope = (math.sin(x1) - math.sin(x0)) / (x1 - x0) intercept = math.sin(x0) - slope * x0 - b.underestimators.add(b.w[i] >= slope * b.x[i] + (intercept - safety_tol) * b.lam[i]) + b.underestimators.add( + b.w[i] >= slope * b.x[i] + (intercept - safety_tol) * b.lam[i] + ) else: - b.underestimators.add(b.w[i] >= math.cos(x0)*b.x[i] + - (math.sin(x0) - x0 * math.cos(x0) - safety_tol)*b.lam[i]) - b.underestimators.add(b.w[i] >= math.cos(x1)*b.x[i] + - (math.sin(x1) - x1 * math.cos(x1) - safety_tol)*b.lam[i]) + b.underestimators.add( + b.w[i] + >= math.cos(x0) * b.x[i] + + (math.sin(x0) - x0 * math.cos(x0) - safety_tol) * b.lam[i] + ) + b.underestimators.add( + b.w[i] + >= math.cos(x1) * b.x[i] + + (math.sin(x1) - x1 * math.cos(x1) - safety_tol) * b.lam[i] + ) return x_pts -def pw_arctan_relaxation(b, x, w, x_pts, relaxation_side=RelaxationSide.BOTH, safety_tol=1e-10): +def pw_arctan_relaxation( + b, x, w, x_pts, relaxation_side=RelaxationSide.BOTH, safety_tol=1e-10 +): """ This function creates piecewise relaxations to relax "w=sin(x)" for -pi/2 <= x <= pi/2. @@ -375,8 +492,16 @@ def pw_arctan_relaxation(b, x, w, x_pts, relaxation_side=RelaxationSide.BOTH, sa if xlb == -math.inf or xub == math.inf: return - OE_tangent_x, OE_tangent_slope, OE_tangent_intercept = _compute_arctan_overestimator_tangent_point(xlb) - UE_tangent_x, UE_tangent_slope, UE_tangent_intercept = _compute_arctan_underestimator_tangent_point(xub) + ( + OE_tangent_x, + OE_tangent_slope, + OE_tangent_intercept, + ) = _compute_arctan_overestimator_tangent_point(xlb) + ( + UE_tangent_x, + UE_tangent_slope, + UE_tangent_intercept, + ) = _compute_arctan_underestimator_tangent_point(xub) non_piecewise_overestimators_pts = [] non_piecewise_underestimator_pts = [] @@ -385,7 +510,9 @@ def pw_arctan_relaxation(b, x, w, x_pts, relaxation_side=RelaxationSide.BOTH, sa new_x_pts = [i for i in x_pts if i < OE_tangent_x] new_x_pts.append(xub) non_piecewise_overestimators_pts = [OE_tangent_x] - non_piecewise_overestimators_pts.extend(i for i in x_pts if i > OE_tangent_x) + non_piecewise_overestimators_pts.extend( + i for i in x_pts if i > OE_tangent_x + ) x_pts = new_x_pts elif relaxation_side == RelaxationSide.UNDER: if UE_tangent_x > xlb: @@ -398,13 +525,17 @@ def pw_arctan_relaxation(b, x, w, x_pts, relaxation_side=RelaxationSide.BOTH, sa b.non_piecewise_overestimators = pyo.ConstraintList() b.non_piecewise_underestimators = pyo.ConstraintList() for pt in non_piecewise_overestimators_pts: - b.non_piecewise_overestimators.add(w <= math.atan(pt) + safety_tol + (x - pt) * _eval.deriv(pt)) + b.non_piecewise_overestimators.add( + w <= math.atan(pt) + safety_tol + (x - pt) * _eval.deriv(pt) + ) for pt in non_piecewise_underestimator_pts: - b.non_piecewise_underestimators.add(w >= math.atan(pt) - safety_tol + (x - pt) * _eval.deriv(pt)) + b.non_piecewise_underestimators.add( + w >= math.atan(pt) - safety_tol + (x - pt) * _eval.deriv(pt) + ) intervals = [] - for i in range(len(x_pts)-1): - intervals.append((x_pts[i], x_pts[i+1])) + for i in range(len(x_pts) - 1): + intervals.append((x_pts[i], x_pts[i + 1])) b.interval_set = pyo.Set(initialize=range(len(intervals))) b.x = pyo.Var(b.interval_set) @@ -434,44 +565,86 @@ def pw_arctan_relaxation(b, x, w, x_pts, relaxation_side=RelaxationSide.BOTH, sa if x0 < 0 and x1 <= 0: slope = (math.atan(x1) - math.atan(x0)) / (x1 - x0) intercept = math.atan(x0) - slope * x0 - b.overestimators.add(b.w[i] <= slope * b.x[i] + (intercept + safety_tol) * b.lam[i]) + b.overestimators.add( + b.w[i] <= slope * b.x[i] + (intercept + safety_tol) * b.lam[i] + ) elif (x0 < 0) and (x1 > 0): - tangent_x, tangent_slope, tangent_intercept = _compute_arctan_overestimator_tangent_point(x0) + ( + tangent_x, + tangent_slope, + tangent_intercept, + ) = _compute_arctan_overestimator_tangent_point(x0) if tangent_x <= x1: - b.overestimators.add(b.w[i] <= tangent_slope * b.x[i] + (tangent_intercept + safety_tol) * b.lam[i]) - b.overestimators.add(b.w[i] <= _eval.deriv(x1)*b.x[i] + - (math.atan(x1) - x1*_eval.deriv(x1) + safety_tol)*b.lam[i]) + b.overestimators.add( + b.w[i] + <= tangent_slope * b.x[i] + + (tangent_intercept + safety_tol) * b.lam[i] + ) + b.overestimators.add( + b.w[i] + <= _eval.deriv(x1) * b.x[i] + + (math.atan(x1) - x1 * _eval.deriv(x1) + safety_tol) * b.lam[i] + ) else: slope = (math.atan(x1) - math.atan(x0)) / (x1 - x0) intercept = math.atan(x0) - slope * x0 - b.overestimators.add(b.w[i] <= slope * b.x[i] + (intercept + safety_tol) * b.lam[i]) + b.overestimators.add( + b.w[i] <= slope * b.x[i] + (intercept + safety_tol) * b.lam[i] + ) else: - b.overestimators.add(b.w[i] <= _eval.deriv(x0)*b.x[i] + - (math.atan(x0) - x0*_eval.deriv(x0) + safety_tol)*b.lam[i]) - b.overestimators.add(b.w[i] <= _eval.deriv(x1)*b.x[i] + - (math.atan(x1) - x1*_eval.deriv(x1) + safety_tol)*b.lam[i]) + b.overestimators.add( + b.w[i] + <= _eval.deriv(x0) * b.x[i] + + (math.atan(x0) - x0 * _eval.deriv(x0) + safety_tol) * b.lam[i] + ) + b.overestimators.add( + b.w[i] + <= _eval.deriv(x1) * b.x[i] + + (math.atan(x1) - x1 * _eval.deriv(x1) + safety_tol) * b.lam[i] + ) # Underestimators if relaxation_side in {RelaxationSide.UNDER, RelaxationSide.BOTH}: if x0 >= 0 and x1 > 0: slope = (math.atan(x1) - math.atan(x0)) / (x1 - x0) intercept = math.atan(x0) - slope * x0 - b.underestimators.add(b.w[i] >= slope * b.x[i] + (intercept - safety_tol) * b.lam[i]) + b.underestimators.add( + b.w[i] >= slope * b.x[i] + (intercept - safety_tol) * b.lam[i] + ) elif (x1 > 0) and (x0 < 0): - tangent_x, tangent_slope, tangent_intercept = _compute_arctan_underestimator_tangent_point(x1) + ( + tangent_x, + tangent_slope, + tangent_intercept, + ) = _compute_arctan_underestimator_tangent_point(x1) if tangent_x >= x0: - b.underestimators.add(b.w[i] >= tangent_slope*b.x[i] + (tangent_intercept - safety_tol)*b.lam[i]) - b.underestimators.add(b.w[i] >= _eval.deriv(x0)*b.x[i] + - (math.atan(x0) - x0*_eval.deriv(x0) - safety_tol)*b.lam[i]) + b.underestimators.add( + b.w[i] + >= tangent_slope * b.x[i] + + (tangent_intercept - safety_tol) * b.lam[i] + ) + b.underestimators.add( + b.w[i] + >= _eval.deriv(x0) * b.x[i] + + (math.atan(x0) - x0 * _eval.deriv(x0) - safety_tol) * b.lam[i] + ) else: slope = (math.atan(x1) - math.atan(x0)) / (x1 - x0) intercept = math.atan(x0) - slope * x0 - b.underestimators.add(b.w[i] >= slope * b.x[i] + (intercept - safety_tol) * b.lam[i]) + b.underestimators.add( + b.w[i] >= slope * b.x[i] + (intercept - safety_tol) * b.lam[i] + ) else: - b.underestimators.add(b.w[i] >= _eval.deriv(x0)*b.x[i] + - (math.atan(x0) - x0*_eval.deriv(x0) - safety_tol)*b.lam[i]) - b.underestimators.add(b.w[i] >= _eval.deriv(x1)*b.x[i] + - (math.atan(x1) - x1*_eval.deriv(x1) - safety_tol)*b.lam[i]) + b.underestimators.add( + b.w[i] + >= _eval.deriv(x0) * b.x[i] + + (math.atan(x0) - x0 * _eval.deriv(x0) - safety_tol) * b.lam[i] + ) + b.underestimators.add( + b.w[i] + >= _eval.deriv(x1) * b.x[i] + + (math.atan(x1) - x1 * _eval.deriv(x1) - safety_tol) * b.lam[i] + ) return x_pts @@ -505,7 +678,7 @@ def _aux_var(self): return self._aux_var_ref.get_component() def get_rhs_vars(self): - return self._x, + return (self._x,) def get_rhs_expr(self): return self._f_x_expr @@ -520,8 +693,19 @@ def vars_with_bounds_in_relaxation(self): res.append(self._x) return res - def set_input(self, x, aux_var, shape, f_x_expr, pw_repn='INC', relaxation_side=RelaxationSide.BOTH, - use_linear_relaxation=True, large_coef=1e5, small_coef=1e-10, safety_tol=1e-10): + def set_input( + self, + x, + aux_var, + shape, + f_x_expr, + pw_repn='INC', + relaxation_side=RelaxationSide.BOTH, + use_linear_relaxation=True, + large_coef=1e5, + small_coef=1e-10, + safety_tol=1e-10, + ): """ Parameters ---------- @@ -541,10 +725,13 @@ def set_input(self, x, aux_var, shape, f_x_expr, pw_repn='INC', relaxation_side= use_linear_relaxation: bool Specifies whether a linear or nonlinear relaxation should be used """ - super().set_input(relaxation_side=relaxation_side, - use_linear_relaxation=use_linear_relaxation, - large_coef=large_coef, small_coef=small_coef, - safety_tol=safety_tol) + super().set_input( + relaxation_side=relaxation_side, + use_linear_relaxation=use_linear_relaxation, + large_coef=large_coef, + small_coef=small_coef, + safety_tol=safety_tol, + ) self._pw_repn = pw_repn self._function_shape = shape self._f_x_expr = f_x_expr @@ -554,8 +741,19 @@ def set_input(self, x, aux_var, shape, f_x_expr, pw_repn='INC', relaxation_side= bnds_list = _get_bnds_list(self._x) self._partitions[self._x] = bnds_list - def build(self, x, aux_var, shape, f_x_expr, pw_repn='INC', relaxation_side=RelaxationSide.BOTH, - use_linear_relaxation=True, large_coef=1e5, small_coef=1e-10, safety_tol=1e-10): + def build( + self, + x, + aux_var, + shape, + f_x_expr, + pw_repn='INC', + relaxation_side=RelaxationSide.BOTH, + use_linear_relaxation=True, + large_coef=1e5, + small_coef=1e-10, + safety_tol=1e-10, + ): """ Parameters ---------- @@ -575,9 +773,18 @@ def build(self, x, aux_var, shape, f_x_expr, pw_repn='INC', relaxation_side=Rela use_linear_relaxation: bool Specifies whether a linear or nonlinear relaxation should be used """ - self.set_input(x=x, aux_var=aux_var, shape=shape, f_x_expr=f_x_expr, pw_repn=pw_repn, - relaxation_side=relaxation_side, use_linear_relaxation=use_linear_relaxation, - large_coef=large_coef, small_coef=small_coef, safety_tol=safety_tol) + self.set_input( + x=x, + aux_var=aux_var, + shape=shape, + f_x_expr=f_x_expr, + pw_repn=pw_repn, + relaxation_side=relaxation_side, + use_linear_relaxation=use_linear_relaxation, + large_coef=large_coef, + small_coef=small_coef, + safety_tol=safety_tol, + ) self.rebuild() def _remove_relaxation(self): @@ -593,7 +800,9 @@ def remove_relaxation(self): self._remove_relaxation() def _needs_secant(self): - if self.relaxation_side == RelaxationSide.BOTH and (self.is_rhs_convex() or self.is_rhs_concave()): + if self.relaxation_side == RelaxationSide.BOTH and ( + self.is_rhs_convex() or self.is_rhs_concave() + ): return True elif self.relaxation_side == RelaxationSide.UNDER and self.is_rhs_concave(): return True @@ -603,8 +812,10 @@ def _needs_secant(self): return False def rebuild(self, build_nonlinear_constraint=False, ensure_oa_at_vertices=True): - super().rebuild(build_nonlinear_constraint=build_nonlinear_constraint, - ensure_oa_at_vertices=ensure_oa_at_vertices) + super().rebuild( + build_nonlinear_constraint=build_nonlinear_constraint, + ensure_oa_at_vertices=ensure_oa_at_vertices, + ) if not build_nonlinear_constraint: if self._check_valid_domain_for_relaxation(): if self._needs_secant(): @@ -628,14 +839,20 @@ def _build_secant(self): del self._secant_expr self._secant_slope = ScalarParam(mutable=True) self._secant_intercept = ScalarParam(mutable=True) - e = LinearExpression(constant=self._secant_intercept, linear_coefs=[self._secant_slope], linear_vars=[self._x]) + e = LinearExpression( + constant=self._secant_intercept, + linear_coefs=[self._secant_slope], + linear_vars=[self._x], + ) self._secant_expr = e if self.is_rhs_concave(): self._secant = ScalarConstraint(expr=self._aux_var >= e) elif self.is_rhs_convex(): self._secant = ScalarConstraint(expr=self._aux_var <= e) else: - raise RuntimeError('Function should be either convex or concave in order to build the secant') + raise RuntimeError( + 'Function should be either convex or concave in order to build the secant' + ) def _update_secant(self): _eval = _FxExpr(self._f_x_expr, self._x) @@ -651,14 +868,16 @@ def _update_secant(self): y1 = _eval(x1) y2 = _eval(x2) slope = (y2 - y1) / (x2 - x1) - intercept = y2 - slope*x2 + intercept = y2 - slope * x2 err_message = None except (ZeroDivisionError, OverflowError, ValueError) as e: slope = None intercept = None err_message = str(e) if err_message is not None: - logger.debug(f'Encountered exception when adding secant for "{self._get_pprint_string()}"; Error message: {err_message}') + logger.debug( + f'Encountered exception when adding secant for "{self._get_pprint_string()}"; Error message: {err_message}' + ) self._remove_relaxation() else: self._secant_slope._value = slope @@ -667,9 +886,13 @@ def _update_secant(self): rel_side = RelaxationSide.UNDER else: rel_side = RelaxationSide.OVER - success, bad_var, bad_coef, err_msg = _check_cut(self._secant_expr, too_small=self.small_coef, - too_large=self.large_coef, relaxation_side=rel_side, - safety_tol=self.safety_tol) + success, bad_var, bad_coef, err_msg = _check_cut( + self._secant_expr, + too_small=self.small_coef, + too_large=self.large_coef, + relaxation_side=rel_side, + safety_tol=self.safety_tol, + ) if not success: self._log_bad_cut(bad_var, bad_coef, err_msg) self._secant.deactivate() @@ -680,15 +903,31 @@ def _build_pw_secant(self): del self._pw_secant self._pw_secant = pe.Block(concrete=True) if self.is_rhs_convex(): - _pw_univariate_relaxation(b=self._pw_secant, x=self._x, w=self._aux_var, x_pts=self._partitions[self._x], - f_x_expr=self._f_x_expr, pw_repn=self._pw_repn, shape=FunctionShape.CONVEX, - relaxation_side=RelaxationSide.OVER, large_eval_tol=self.large_coef, - safety_tol=self.safety_tol) + _pw_univariate_relaxation( + b=self._pw_secant, + x=self._x, + w=self._aux_var, + x_pts=self._partitions[self._x], + f_x_expr=self._f_x_expr, + pw_repn=self._pw_repn, + shape=FunctionShape.CONVEX, + relaxation_side=RelaxationSide.OVER, + large_eval_tol=self.large_coef, + safety_tol=self.safety_tol, + ) else: - _pw_univariate_relaxation(b=self._pw_secant, x=self._x, w=self._aux_var, x_pts=self._partitions[self._x], - f_x_expr=self._f_x_expr, pw_repn=self._pw_repn, shape=FunctionShape.CONCAVE, - relaxation_side=RelaxationSide.UNDER, large_eval_tol=self.large_coef, - safety_tol=self.safety_tol) + _pw_univariate_relaxation( + b=self._pw_secant, + x=self._x, + w=self._aux_var, + x_pts=self._partitions[self._x], + f_x_expr=self._f_x_expr, + pw_repn=self._pw_repn, + shape=FunctionShape.CONCAVE, + relaxation_side=RelaxationSide.UNDER, + large_eval_tol=self.large_coef, + safety_tol=self.safety_tol, + ) def add_partition_point(self, value=None): """ @@ -741,8 +980,17 @@ class CustomUnivariateBaseRelaxationData(PWUnivariateRelaxationData): def _rhs_func(self, x): raise NotImplementedError('This should be implemented by a derived class') - def set_input(self, x, aux_var, pw_repn='INC', relaxation_side=RelaxationSide.BOTH, - use_linear_relaxation=True, large_coef=1e5, small_coef=1e-10, safety_tol=1e-10): + def set_input( + self, + x, + aux_var, + pw_repn='INC', + relaxation_side=RelaxationSide.BOTH, + use_linear_relaxation=True, + large_coef=1e5, + small_coef=1e-10, + safety_tol=1e-10, + ): """ Parameters ---------- @@ -758,15 +1006,30 @@ def set_input(self, x, aux_var, pw_repn='INC', relaxation_side=RelaxationSide.BO use_linear_relaxation: bool Specifies whether a linear or nonlinear relaxation should be used """ - super().set_input(x=x, aux_var=aux_var, shape=FunctionShape.UNKNOWN, - f_x_expr=self._rhs_func(x), pw_repn=pw_repn, - relaxation_side=relaxation_side, - use_linear_relaxation=use_linear_relaxation, - large_coef=large_coef, small_coef=small_coef, - safety_tol=safety_tol) - - def build(self, x, aux_var, pw_repn='INC', relaxation_side=RelaxationSide.BOTH, - use_linear_relaxation=True, large_coef=1e5, small_coef=1e-10, safety_tol=1e-10): + super().set_input( + x=x, + aux_var=aux_var, + shape=FunctionShape.UNKNOWN, + f_x_expr=self._rhs_func(x), + pw_repn=pw_repn, + relaxation_side=relaxation_side, + use_linear_relaxation=use_linear_relaxation, + large_coef=large_coef, + small_coef=small_coef, + safety_tol=safety_tol, + ) + + def build( + self, + x, + aux_var, + pw_repn='INC', + relaxation_side=RelaxationSide.BOTH, + use_linear_relaxation=True, + large_coef=1e5, + small_coef=1e-10, + safety_tol=1e-10, + ): """ Parameters ---------- @@ -782,9 +1045,16 @@ def build(self, x, aux_var, pw_repn='INC', relaxation_side=RelaxationSide.BOTH, use_linear_relaxation: bool Specifies whether a linear or nonlinear relaxation should be used """ - self.set_input(x=x, aux_var=aux_var, pw_repn=pw_repn, relaxation_side=relaxation_side, - use_linear_relaxation=use_linear_relaxation, large_coef=large_coef, small_coef=small_coef, - safety_tol=safety_tol) + self.set_input( + x=x, + aux_var=aux_var, + pw_repn=pw_repn, + relaxation_side=relaxation_side, + use_linear_relaxation=use_linear_relaxation, + large_coef=large_coef, + small_coef=small_coef, + safety_tol=safety_tol, + ) self.rebuild() @@ -793,6 +1063,7 @@ class PWXSquaredRelaxationData(CustomUnivariateBaseRelaxationData): """ A helper class for building and modifying piecewise relaxations of aux_var = x**2. """ + def _rhs_func(self, x): return x**2 @@ -815,15 +1086,17 @@ def rebuild(self, build_nonlinear_constraint=False, ensure_oa_at_vertices=True): if current_concave != self._last_concave: self._needs_rebuilt = True self._last_concave = current_concave - super().rebuild(build_nonlinear_constraint=build_nonlinear_constraint, - ensure_oa_at_vertices=ensure_oa_at_vertices) + super().rebuild( + build_nonlinear_constraint=build_nonlinear_constraint, + ensure_oa_at_vertices=ensure_oa_at_vertices, + ) def _rhs_func(self, x): return pe.cos(x) def is_rhs_concave(self): lb, ub = tuple(_get_bnds_list(self._x)) - if lb >= -math.pi/2 and ub <= math.pi/2: + if lb >= -math.pi / 2 and ub <= math.pi / 2: return True else: return False @@ -868,8 +1141,10 @@ def rebuild(self, build_nonlinear_constraint=False, ensure_oa_at_vertices=True): self._needs_rebuilt = True self._last_convex = current_convex self._last_concave = current_concave - super().rebuild(build_nonlinear_constraint=build_nonlinear_constraint, - ensure_oa_at_vertices=ensure_oa_at_vertices) + super().rebuild( + build_nonlinear_constraint=build_nonlinear_constraint, + ensure_oa_at_vertices=ensure_oa_at_vertices, + ) if not build_nonlinear_constraint: if self._check_valid_domain_for_relaxation(): if (not self.is_rhs_convex()) and (not self.is_rhs_concave()): @@ -882,9 +1157,14 @@ def rebuild(self, build_nonlinear_constraint=False, ensure_oa_at_vertices=True): self._remove_relaxation() del self._pw_secant self._pw_secant = pe.Block(concrete=True) - self._pw_func()(b=self._pw_secant, x=self._x, w=self._aux_var, x_pts=self._partitions[self._x], - relaxation_side=self.relaxation_side, - safety_tol=self.safety_tol) + self._pw_func()( + b=self._pw_secant, + x=self._x, + w=self._aux_var, + x_pts=self._partitions[self._x], + relaxation_side=self.relaxation_side, + safety_tol=self.safety_tol, + ) else: self._remove_relaxation() @@ -897,14 +1177,20 @@ def _build_relaxation(self): self._secant = IndexedConstraint(self._secant_index) if self.relaxation_side in {RelaxationSide.BOTH, RelaxationSide.UNDER}: for ndx in [0, 1]: - e = LinearExpression(constant=self._secant_intercept[ndx], - linear_coefs=[self._secant_slope[ndx]], linear_vars=[self._x]) + e = LinearExpression( + constant=self._secant_intercept[ndx], + linear_coefs=[self._secant_slope[ndx]], + linear_vars=[self._x], + ) self._secant_exprs[ndx] = e self._secant[ndx] = self._aux_var >= e if self.relaxation_side in {RelaxationSide.BOTH, RelaxationSide.OVER}: for ndx in [2, 3]: - e = LinearExpression(constant=self._secant_intercept[ndx], - linear_coefs=[self._secant_slope[ndx]], linear_vars=[self._x]) + e = LinearExpression( + constant=self._secant_intercept[ndx], + linear_coefs=[self._secant_slope[ndx]], + linear_vars=[self._x], + ) self._secant_exprs[ndx] = e self._secant[ndx] = self._aux_var <= e @@ -913,9 +1199,13 @@ def _check_expr(self, ndx): rel_side = RelaxationSide.UNDER else: rel_side = RelaxationSide.OVER - success, bad_var, bad_coef, err_msg = _check_cut(self._secant_exprs[ndx], too_small=self.small_coef, - too_large=self.large_coef, relaxation_side=rel_side, - safety_tol=self.safety_tol) + success, bad_var, bad_coef, err_msg = _check_cut( + self._secant_exprs[ndx], + too_small=self.small_coef, + too_large=self.large_coef, + relaxation_side=rel_side, + safety_tol=self.safety_tol, + ) if not success: self._log_bad_cut(bad_var, bad_coef, err_msg) self._secant[ndx].deactivate() diff --git a/pyomo/contrib/coramin/third_party/__init__.py b/pyomo/contrib/coramin/third_party/__init__.py index 53ac6c77e5d..19f45f06b56 100644 --- a/pyomo/contrib/coramin/third_party/__init__.py +++ b/pyomo/contrib/coramin/third_party/__init__.py @@ -1 +1,5 @@ -from .minlplib_tools import get_minlplib_instancedata, filter_minlplib_instances, get_minlplib +from .minlplib_tools import ( + get_minlplib_instancedata, + filter_minlplib_instances, + get_minlplib, +) diff --git a/pyomo/contrib/coramin/third_party/minlplib_tools.py b/pyomo/contrib/coramin/third_party/minlplib_tools.py index a39054bd5fb..e167d9f340a 100644 --- a/pyomo/contrib/coramin/third_party/minlplib_tools.py +++ b/pyomo/contrib/coramin/third_party/minlplib_tools.py @@ -19,8 +19,8 @@ def get_minlplib_instancedata(target_filename=None): Parameters ---------- target_filename: str - The full path, including the filename for where to place the downloaded - file. The default will be a directory called minlplib in the current + The full path, including the filename for where to place the downloaded + file. The default will be a directory called minlplib in the current working directory and a filename of instancedata.csv. """ if target_filename is None: @@ -28,8 +28,10 @@ def get_minlplib_instancedata(target_filename=None): download_dir = os.path.dirname(target_filename) if os.path.exists(target_filename): - raise ValueError('A file named {filename} already exists.'.format(filename=target_filename)) - + raise ValueError( + 'A file named {filename} already exists.'.format(filename=target_filename) + ) + if not os.path.exists(download_dir): os.makedirs(download_dir) @@ -59,67 +61,105 @@ def _process_acceptable_arg(name, arg, default): def _check_int_arg(arg, _min, _max, arg_name, case_name): if arg < _min or arg > _max: - logger.debug('excluding {case_name} due to {arg_name}'.format(case_name=case_name, arg_name=arg_name)) + logger.debug( + 'excluding {case_name} due to {arg_name}'.format( + case_name=case_name, arg_name=arg_name + ) + ) return True return False def _check_acceptable(arg, acceptable_set, arg_name, case_name): if arg not in acceptable_set: - logger.debug('excluding {case_name} due to {arg_name}'.format(case_name=case_name, arg_name=arg_name)) + logger.debug( + 'excluding {case_name} due to {arg_name}'.format( + case_name=case_name, arg_name=arg_name + ) + ) return True return False -def filter_minlplib_instances(instancedata_filename=None, - min_nvars=0, max_nvars=math.inf, - min_nbinvars=0, max_nbinvars=math.inf, - min_nintvars=0, max_nintvars=math.inf, - min_nnlvars=0, max_nnlvars=math.inf, - min_nnlbinvars=0, max_nnlbinvars=math.inf, - min_nnlintvars=0, max_nnlintvars=math.inf, - min_nobjnz=0, max_nobjnz=math.inf, - min_nobjnlnz=0, max_nobjnlnz=math.inf, - min_ncons=0, max_ncons=math.inf, - min_nlincons=0, max_nlincons=math.inf, - min_nquadcons=0, max_nquadcons=math.inf, - min_npolynomcons=0, max_npolynomcons=math.inf, - min_nsignomcons=0, max_nsignomcons=math.inf, - min_ngennlcons=0, max_ngennlcons=math.inf, - min_njacobiannz=0, max_njacobiannz=math.inf, - min_njacobiannlnz=0, max_njacobiannlnz=math.inf, - min_nlaghessiannz=0, max_nlaghessiannz=math.inf, - min_nlaghessiandiagnz=0, max_nlaghessiandiagnz=math.inf, - min_nsemi=0, max_nsemi=math.inf, - min_nnlsemi=0, max_nnlsemi=math.inf, - min_nsos1=0, max_nsos1=math.inf, - min_nsos2=0, max_nsos2=math.inf, - acceptable_formats=None, - acceptable_probtype=None, - acceptable_objtype=None, - acceptable_objcurvature=None, - acceptable_conscurvature=None, - acceptable_convex=None): +def filter_minlplib_instances( + instancedata_filename=None, + min_nvars=0, + max_nvars=math.inf, + min_nbinvars=0, + max_nbinvars=math.inf, + min_nintvars=0, + max_nintvars=math.inf, + min_nnlvars=0, + max_nnlvars=math.inf, + min_nnlbinvars=0, + max_nnlbinvars=math.inf, + min_nnlintvars=0, + max_nnlintvars=math.inf, + min_nobjnz=0, + max_nobjnz=math.inf, + min_nobjnlnz=0, + max_nobjnlnz=math.inf, + min_ncons=0, + max_ncons=math.inf, + min_nlincons=0, + max_nlincons=math.inf, + min_nquadcons=0, + max_nquadcons=math.inf, + min_npolynomcons=0, + max_npolynomcons=math.inf, + min_nsignomcons=0, + max_nsignomcons=math.inf, + min_ngennlcons=0, + max_ngennlcons=math.inf, + min_njacobiannz=0, + max_njacobiannz=math.inf, + min_njacobiannlnz=0, + max_njacobiannlnz=math.inf, + min_nlaghessiannz=0, + max_nlaghessiannz=math.inf, + min_nlaghessiandiagnz=0, + max_nlaghessiandiagnz=math.inf, + min_nsemi=0, + max_nsemi=math.inf, + min_nnlsemi=0, + max_nnlsemi=math.inf, + min_nsos1=0, + max_nsos1=math.inf, + min_nsos2=0, + max_nsos2=math.inf, + acceptable_formats=None, + acceptable_probtype=None, + acceptable_objtype=None, + acceptable_objcurvature=None, + acceptable_conscurvature=None, + acceptable_convex=None, +): """ This function filters problems from MINLPLib based on instancedata.csv from MINLPLib and the conditions specified through the function arguments. The function argument names - correspond to column headings from instancedata.csv. The - arguments starting with min or max require int or float inputs. - The arguments starting with acceptable require either a - string or an iterable of strings. See the MINLPLib documentation + correspond to column headings from instancedata.csv. The + arguments starting with min or max require int or float inputs. + The arguments starting with acceptable require either a + string or an iterable of strings. See the MINLPLib documentation for acceptable values. """ if instancedata_filename is None: - instancedata_filename = os.path.join(os.getcwd(), 'minlplib', 'instancedata.csv') + instancedata_filename = os.path.join( + os.getcwd(), 'minlplib', 'instancedata.csv' + ) if not os.path.exists(instancedata_filename): - raise RuntimeError('{filename} does not exist. Please use get_minlplib_instancedata() first or specify the location of the MINLPLib instancedata.csv with the instancedata_filename argument.'.format(filename=instancedata_filename)) + raise RuntimeError( + '{filename} does not exist. Please use get_minlplib_instancedata() first or specify the location of the MINLPLib instancedata.csv with the instancedata_filename argument.'.format( + filename=instancedata_filename + ) + ) acceptable_formats = _process_acceptable_arg( 'acceptable_formats', acceptable_formats, - set(['ams', 'gms', 'lp', 'mod', 'nl', 'osil', 'pip']) + set(['ams', 'gms', 'lp', 'mod', 'nl', 'osil', 'pip']), ) default_acceptable_probtype = set() @@ -127,34 +167,52 @@ def filter_minlplib_instances(instancedata_filename=None, for post in ['NLP', 'QCQP', 'QP', 'QCP', 'P']: default_acceptable_probtype.add(pre + post) acceptable_probtype = _process_acceptable_arg( - 'acceptable_probtype', - acceptable_probtype, - default_acceptable_probtype - ) + 'acceptable_probtype', acceptable_probtype, default_acceptable_probtype + ) acceptable_objtype = _process_acceptable_arg( 'acceptable_objtype', acceptable_objtype, - set(['constant', 'linear', 'quadratic', 'polynomial', 'signomial', 'nonlinear']) - ) + set( + ['constant', 'linear', 'quadratic', 'polynomial', 'signomial', 'nonlinear'] + ), + ) acceptable_objcurvature = _process_acceptable_arg( 'acceptable_objcurvature', acceptable_objcurvature, - set(['linear', 'convex', 'concave', 'indefinite', 'nonconvex', 'nonconcave', 'unknown']) - ) + set( + [ + 'linear', + 'convex', + 'concave', + 'indefinite', + 'nonconvex', + 'nonconcave', + 'unknown', + ] + ), + ) acceptable_conscurvature = _process_acceptable_arg( 'acceptable_conscurvature', acceptable_conscurvature, - set(['linear', 'convex', 'concave', 'indefinite', 'nonconvex', 'nonconcave', 'unknown']) - ) + set( + [ + 'linear', + 'convex', + 'concave', + 'indefinite', + 'nonconvex', + 'nonconcave', + 'unknown', + ] + ), + ) acceptable_convex = _process_acceptable_arg( - 'acceptable_convex', - acceptable_convex, - set(['True', 'False', '']) - ) + 'acceptable_convex', acceptable_convex, set(['True', 'False', '']) + ) int_arg_name_list = [ 'nvars', @@ -234,14 +292,14 @@ def filter_minlplib_instances(instancedata_filename=None, 'objtype', 'objcurvature', 'conscurvature', - 'convex' + 'convex', ] acceptable_set_list = [ acceptable_probtype, acceptable_objtype, acceptable_objcurvature, acceptable_conscurvature, - acceptable_convex + acceptable_convex, ] with open(instancedata_filename, 'r') as csv_file: @@ -253,7 +311,7 @@ def filter_minlplib_instances(instancedata_filename=None, for ndx, row in enumerate(rows): if len(row) == 0: continue - + case_name = row[headings['name']] available_formats = row[headings['formats']] @@ -269,27 +327,29 @@ def filter_minlplib_instances(instancedata_filename=None, should_continue = False if len(acceptable_formats.intersection(available_formats)) == 0: - logger.debug('excluding {case} due to available_formats'.format(case=case_name)) + logger.debug( + 'excluding {case} due to available_formats'.format(case=case_name) + ) should_continue = True for ndx, acceptable_arg_name in enumerate(acceptable_arg_name_list): acceptable_set = acceptable_set_list[ndx] arg = row[headings[acceptable_arg_name]] - if _check_acceptable(arg=arg, - acceptable_set=acceptable_set, - arg_name=acceptable_arg_name, - case_name=case_name): + if _check_acceptable( + arg=arg, + acceptable_set=acceptable_set, + arg_name=acceptable_arg_name, + case_name=case_name, + ): should_continue = True for ndx, arg_name in enumerate(int_arg_name_list): _min = min_list[ndx] _max = max_list[ndx] arg = int(row[headings[arg_name]]) - if _check_int_arg(arg=arg, - _min=_min, - _max=_max, - arg_name=arg_name, - case_name=case_name): + if _check_int_arg( + arg=arg, _min=_min, _max=_max, arg_name=arg_name, case_name=case_name + ): should_continue = True if should_continue: @@ -307,16 +367,16 @@ def get_minlplib(download_dir=None, format='osil', problem_name=None): Parameters ---------- download_dir: str - The directory in which to place the downloaded files. The default will be a + The directory in which to place the downloaded files. The default will be a current_working_directory/minlplib/file_format/. format: str The file format requested. Options are ams, gms, lp, mod, nl, osil, and pip problem_name: None or str - If problem_name is None, then the entire zip file will be downloaded - and extracted (all problems with the specified format). If a single problem - needs to be downloaded, then the name of the problem can be specified. - This can be significantly faster than downloading all of the problems. - However, individual problems are not compressed, so downloading multiple + If problem_name is None, then the entire zip file will be downloaded + and extracted (all problems with the specified format). If a single problem + needs to be downloaded, then the name of the problem can be specified. + This can be significantly faster than downloading all of the problems. + However, individual problems are not compressed, so downloading multiple individual problems can quickly become expensive. """ if download_dir is None: @@ -325,18 +385,26 @@ def get_minlplib(download_dir=None, format='osil', problem_name=None): if problem_name is None: if os.path.exists(download_dir): raise ValueError( - 'The specified download_dir already exists: ' + download_dir) + 'The specified download_dir already exists: ' + download_dir + ) os.makedirs(download_dir) downloader = download.FileDownloader() - zip_dirname = os.path.join(download_dir, 'minlplib_'+format) + zip_dirname = os.path.join(download_dir, 'minlplib_' + format) downloader.set_destination_filename(zip_dirname) - downloader.get_zip_archive('http://www.minlplib.org/minlplib_'+format+'.zip') - for i in os.listdir(os.path.join(download_dir, 'minlplib_'+format, 'minlplib', format)): - os.rename(os.path.join(download_dir, 'minlplib_'+format, 'minlplib', format, i), os.path.join(download_dir, i)) - os.rmdir(os.path.join(download_dir, 'minlplib_'+format, 'minlplib', format)) - os.rmdir(os.path.join(download_dir, 'minlplib_'+format, 'minlplib')) - os.rmdir(os.path.join(download_dir, 'minlplib_'+format)) + downloader.get_zip_archive( + 'http://www.minlplib.org/minlplib_' + format + '.zip' + ) + for i in os.listdir( + os.path.join(download_dir, 'minlplib_' + format, 'minlplib', format) + ): + os.rename( + os.path.join(download_dir, 'minlplib_' + format, 'minlplib', format, i), + os.path.join(download_dir, i), + ) + os.rmdir(os.path.join(download_dir, 'minlplib_' + format, 'minlplib', format)) + os.rmdir(os.path.join(download_dir, 'minlplib_' + format, 'minlplib')) + os.rmdir(os.path.join(download_dir, 'minlplib_' + format)) else: if not os.path.exists(download_dir): os.makedirs(download_dir) @@ -345,5 +413,6 @@ def get_minlplib(download_dir=None, format='osil', problem_name=None): raise ValueError(f'The target filename ({target_filename}) already exists') downloader = download.FileDownloader() downloader.set_destination_filename(target_filename) - downloader.get_binary_file('http://www.minlplib.org/'+format+'/'+problem_name+'.'+format) - + downloader.get_binary_file( + 'http://www.minlplib.org/' + format + '/' + problem_name + '.' + format + ) diff --git a/pyomo/contrib/coramin/third_party/tests/test_minlplib_tools.py b/pyomo/contrib/coramin/third_party/tests/test_minlplib_tools.py index 93d0c928776..7eac973f830 100644 --- a/pyomo/contrib/coramin/third_party/tests/test_minlplib_tools.py +++ b/pyomo/contrib/coramin/third_party/tests/test_minlplib_tools.py @@ -16,7 +16,9 @@ class TestMINLPLibTools(unittest.TestCase): def test_get_minlplib_instancedata(self): current_dir = os.getcwd() coramin.third_party.get_minlplib_instancedata() - self.assertTrue(os.path.exists(os.path.join(current_dir, 'minlplib', 'instancedata.csv'))) + self.assertTrue( + os.path.exists(os.path.join(current_dir, 'minlplib', 'instancedata.csv')) + ) cases = coramin.third_party.filter_minlplib_instances() self.assertEqual(len(cases), 1595) os.remove(os.path.join(current_dir, 'minlplib', 'instancedata.csv')) @@ -24,128 +26,242 @@ def test_get_minlplib_instancedata(self): def test_filter_minlplib_instances(self): current_dir = this_file_dir() - coramin.third_party.get_minlplib_instancedata(target_filename=os.path.join(current_dir, 'minlplib', 'instancedata.csv')) + coramin.third_party.get_minlplib_instancedata( + target_filename=os.path.join(current_dir, 'minlplib', 'instancedata.csv') + ) total_cases = 1595 - cases = coramin.third_party.filter_minlplib_instances(instancedata_filename=os.path.join(current_dir, 'minlplib', 'instancedata.csv'), - acceptable_formats='osil', - acceptable_probtype='QCQP', - min_njacobiannz=1000, - max_njacobiannz=10000) + cases = coramin.third_party.filter_minlplib_instances( + instancedata_filename=os.path.join( + current_dir, 'minlplib', 'instancedata.csv' + ), + acceptable_formats='osil', + acceptable_probtype='QCQP', + min_njacobiannz=1000, + max_njacobiannz=10000, + ) self.assertEqual(len(cases), 6) # regression - cases = coramin.third_party.filter_minlplib_instances(instancedata_filename=os.path.join(current_dir, 'minlplib', 'instancedata.csv'), - acceptable_formats=['osil', 'gms']) + cases = coramin.third_party.filter_minlplib_instances( + instancedata_filename=os.path.join( + current_dir, 'minlplib', 'instancedata.csv' + ), + acceptable_formats=['osil', 'gms'], + ) self.assertEqual(len(cases), total_cases) # unit - cases = coramin.third_party.filter_minlplib_instances(instancedata_filename=os.path.join(current_dir, 'minlplib', 'instancedata.csv')) + cases = coramin.third_party.filter_minlplib_instances( + instancedata_filename=os.path.join( + current_dir, 'minlplib', 'instancedata.csv' + ) + ) self.assertEqual(len(cases), total_cases) # unit - cases = coramin.third_party.filter_minlplib_instances(instancedata_filename=os.path.join(current_dir, 'minlplib', 'instancedata.csv'), - acceptable_probtype=['QCQP', 'MIQCQP', 'MBQCQP']) + cases = coramin.third_party.filter_minlplib_instances( + instancedata_filename=os.path.join( + current_dir, 'minlplib', 'instancedata.csv' + ), + acceptable_probtype=['QCQP', 'MIQCQP', 'MBQCQP'], + ) self.assertEqual(len(cases), 56) # regression - cases = coramin.third_party.filter_minlplib_instances(instancedata_filename=os.path.join(current_dir, 'minlplib', 'instancedata.csv'), - acceptable_objtype='linear', - acceptable_objcurvature='linear', - acceptable_conscurvature='convex', - acceptable_convex=True) + cases = coramin.third_party.filter_minlplib_instances( + instancedata_filename=os.path.join( + current_dir, 'minlplib', 'instancedata.csv' + ), + acceptable_objtype='linear', + acceptable_objcurvature='linear', + acceptable_conscurvature='convex', + acceptable_convex=True, + ) self.assertEqual(len(cases), 280) # unit - cases = coramin.third_party.filter_minlplib_instances(instancedata_filename=os.path.join(current_dir, 'minlplib', 'instancedata.csv'), - acceptable_convex=[True]) + cases = coramin.third_party.filter_minlplib_instances( + instancedata_filename=os.path.join( + current_dir, 'minlplib', 'instancedata.csv' + ), + acceptable_convex=[True], + ) self.assertEqual(len(cases), 377) # unit - cases = coramin.third_party.filter_minlplib_instances(instancedata_filename=os.path.join(current_dir, 'minlplib', 'instancedata.csv'), - min_nvars=2, max_nvars=200000) - self.assertEqual(len(cases), total_cases-16-1) # unit - - cases = coramin.third_party.filter_minlplib_instances(instancedata_filename=os.path.join(current_dir, 'minlplib', 'instancedata.csv'), - max_nbinvars=31000) - self.assertEqual(len(cases), total_cases-1) # unit - - cases = coramin.third_party.filter_minlplib_instances(instancedata_filename=os.path.join(current_dir, 'minlplib', 'instancedata.csv'), - max_nintvars=1999) - self.assertEqual(len(cases), total_cases-1) # unit - - cases = coramin.third_party.filter_minlplib_instances(instancedata_filename=os.path.join(current_dir, 'minlplib', 'instancedata.csv'), - max_ncons=164000) - self.assertEqual(len(cases), total_cases-1) # unit - - cases = coramin.third_party.filter_minlplib_instances(instancedata_filename=os.path.join(current_dir, 'minlplib', 'instancedata.csv'), - max_nsemi=13) + cases = coramin.third_party.filter_minlplib_instances( + instancedata_filename=os.path.join( + current_dir, 'minlplib', 'instancedata.csv' + ), + min_nvars=2, + max_nvars=200000, + ) + self.assertEqual(len(cases), total_cases - 16 - 1) # unit + + cases = coramin.third_party.filter_minlplib_instances( + instancedata_filename=os.path.join( + current_dir, 'minlplib', 'instancedata.csv' + ), + max_nbinvars=31000, + ) + self.assertEqual(len(cases), total_cases - 1) # unit + + cases = coramin.third_party.filter_minlplib_instances( + instancedata_filename=os.path.join( + current_dir, 'minlplib', 'instancedata.csv' + ), + max_nintvars=1999, + ) + self.assertEqual(len(cases), total_cases - 1) # unit + + cases = coramin.third_party.filter_minlplib_instances( + instancedata_filename=os.path.join( + current_dir, 'minlplib', 'instancedata.csv' + ), + max_ncons=164000, + ) + self.assertEqual(len(cases), total_cases - 1) # unit + + cases = coramin.third_party.filter_minlplib_instances( + instancedata_filename=os.path.join( + current_dir, 'minlplib', 'instancedata.csv' + ), + max_nsemi=13, + ) self.assertEqual(len(cases), total_cases) # unit - cases = coramin.third_party.filter_minlplib_instances(instancedata_filename=os.path.join(current_dir, 'minlplib', 'instancedata.csv'), - max_nsos1=0, max_nsos2=0) - self.assertEqual(len(cases), total_cases-6) # unit - - cases = coramin.third_party.filter_minlplib_instances(instancedata_filename=os.path.join(current_dir, 'minlplib', 'instancedata.csv'), - max_nnlvars=199998) - self.assertEqual(len(cases), total_cases-1) # unit - - cases = coramin.third_party.filter_minlplib_instances(instancedata_filename=os.path.join(current_dir, 'minlplib', 'instancedata.csv'), - max_nnlbinvars=23867) - self.assertEqual(len(cases), total_cases-1) # unit - - cases = coramin.third_party.filter_minlplib_instances(instancedata_filename=os.path.join(current_dir, 'minlplib', 'instancedata.csv'), - max_nnlintvars=1999) - self.assertEqual(len(cases), total_cases-1) # unit - - cases = coramin.third_party.filter_minlplib_instances(instancedata_filename=os.path.join(current_dir, 'minlplib', 'instancedata.csv'), - max_nobjnz=99997) - self.assertEqual(len(cases), total_cases-1) # unit - - cases = coramin.third_party.filter_minlplib_instances(instancedata_filename=os.path.join(current_dir, 'minlplib', 'instancedata.csv'), - max_nobjnlnz=99997) - self.assertEqual(len(cases), total_cases-1) # unit - - cases = coramin.third_party.filter_minlplib_instances(instancedata_filename=os.path.join(current_dir, 'minlplib', 'instancedata.csv'), - max_nlincons=164319) - self.assertEqual(len(cases), total_cases-1) # unit - - cases = coramin.third_party.filter_minlplib_instances(instancedata_filename=os.path.join(current_dir, 'minlplib', 'instancedata.csv'), - max_nquadcons=139999) - self.assertEqual(len(cases), total_cases-1) # unit - - cases = coramin.third_party.filter_minlplib_instances(instancedata_filename=os.path.join(current_dir, 'minlplib', 'instancedata.csv'), - max_npolynomcons=13975) - self.assertEqual(len(cases), total_cases-1) # unit - - cases = coramin.third_party.filter_minlplib_instances(instancedata_filename=os.path.join(current_dir, 'minlplib', 'instancedata.csv'), - max_nsignomcons=801) - self.assertEqual(len(cases), total_cases-1) # unit - - cases = coramin.third_party.filter_minlplib_instances(instancedata_filename=os.path.join(current_dir, 'minlplib', 'instancedata.csv'), - max_ngennlcons=13975) - self.assertEqual(len(cases), total_cases-2) # unit - - cases = coramin.third_party.filter_minlplib_instances(instancedata_filename=os.path.join(current_dir, 'minlplib', 'instancedata.csv'), - max_njacobiannlnz=1623023) - self.assertEqual(len(cases), total_cases-1) # unit - - cases = coramin.third_party.filter_minlplib_instances(instancedata_filename=os.path.join(current_dir, 'minlplib', 'instancedata.csv'), - max_nlaghessiannz=1825419) - self.assertEqual(len(cases), total_cases-1) # unit - - cases = coramin.third_party.filter_minlplib_instances(instancedata_filename=os.path.join(current_dir, 'minlplib', 'instancedata.csv'), - max_nlaghessiandiagnz=100000) - self.assertEqual(len(cases), total_cases-1) # unit - - cases = coramin.third_party.filter_minlplib_instances(instancedata_filename=os.path.join(current_dir, 'minlplib', 'instancedata.csv'), - min_nnlsemi=1) + cases = coramin.third_party.filter_minlplib_instances( + instancedata_filename=os.path.join( + current_dir, 'minlplib', 'instancedata.csv' + ), + max_nsos1=0, + max_nsos2=0, + ) + self.assertEqual(len(cases), total_cases - 6) # unit + + cases = coramin.third_party.filter_minlplib_instances( + instancedata_filename=os.path.join( + current_dir, 'minlplib', 'instancedata.csv' + ), + max_nnlvars=199998, + ) + self.assertEqual(len(cases), total_cases - 1) # unit + + cases = coramin.third_party.filter_minlplib_instances( + instancedata_filename=os.path.join( + current_dir, 'minlplib', 'instancedata.csv' + ), + max_nnlbinvars=23867, + ) + self.assertEqual(len(cases), total_cases - 1) # unit + + cases = coramin.third_party.filter_minlplib_instances( + instancedata_filename=os.path.join( + current_dir, 'minlplib', 'instancedata.csv' + ), + max_nnlintvars=1999, + ) + self.assertEqual(len(cases), total_cases - 1) # unit + + cases = coramin.third_party.filter_minlplib_instances( + instancedata_filename=os.path.join( + current_dir, 'minlplib', 'instancedata.csv' + ), + max_nobjnz=99997, + ) + self.assertEqual(len(cases), total_cases - 1) # unit + + cases = coramin.third_party.filter_minlplib_instances( + instancedata_filename=os.path.join( + current_dir, 'minlplib', 'instancedata.csv' + ), + max_nobjnlnz=99997, + ) + self.assertEqual(len(cases), total_cases - 1) # unit + + cases = coramin.third_party.filter_minlplib_instances( + instancedata_filename=os.path.join( + current_dir, 'minlplib', 'instancedata.csv' + ), + max_nlincons=164319, + ) + self.assertEqual(len(cases), total_cases - 1) # unit + + cases = coramin.third_party.filter_minlplib_instances( + instancedata_filename=os.path.join( + current_dir, 'minlplib', 'instancedata.csv' + ), + max_nquadcons=139999, + ) + self.assertEqual(len(cases), total_cases - 1) # unit + + cases = coramin.third_party.filter_minlplib_instances( + instancedata_filename=os.path.join( + current_dir, 'minlplib', 'instancedata.csv' + ), + max_npolynomcons=13975, + ) + self.assertEqual(len(cases), total_cases - 1) # unit + + cases = coramin.third_party.filter_minlplib_instances( + instancedata_filename=os.path.join( + current_dir, 'minlplib', 'instancedata.csv' + ), + max_nsignomcons=801, + ) + self.assertEqual(len(cases), total_cases - 1) # unit + + cases = coramin.third_party.filter_minlplib_instances( + instancedata_filename=os.path.join( + current_dir, 'minlplib', 'instancedata.csv' + ), + max_ngennlcons=13975, + ) + self.assertEqual(len(cases), total_cases - 2) # unit + + cases = coramin.third_party.filter_minlplib_instances( + instancedata_filename=os.path.join( + current_dir, 'minlplib', 'instancedata.csv' + ), + max_njacobiannlnz=1623023, + ) + self.assertEqual(len(cases), total_cases - 1) # unit + + cases = coramin.third_party.filter_minlplib_instances( + instancedata_filename=os.path.join( + current_dir, 'minlplib', 'instancedata.csv' + ), + max_nlaghessiannz=1825419, + ) + self.assertEqual(len(cases), total_cases - 1) # unit + + cases = coramin.third_party.filter_minlplib_instances( + instancedata_filename=os.path.join( + current_dir, 'minlplib', 'instancedata.csv' + ), + max_nlaghessiandiagnz=100000, + ) + self.assertEqual(len(cases), total_cases - 1) # unit + + cases = coramin.third_party.filter_minlplib_instances( + instancedata_filename=os.path.join( + current_dir, 'minlplib', 'instancedata.csv' + ), + min_nnlsemi=1, + ) self.assertEqual(len(cases), 0) # unit - cases = coramin.third_party.filter_minlplib_instances(instancedata_filename=os.path.join(current_dir, 'minlplib', 'instancedata.csv'), - acceptable_objcurvature=['linear', 'convex']) + cases = coramin.third_party.filter_minlplib_instances( + instancedata_filename=os.path.join( + current_dir, 'minlplib', 'instancedata.csv' + ), + acceptable_objcurvature=['linear', 'convex'], + ) self.assertEqual(len(cases), 1220) # unit - + os.remove(os.path.join(current_dir, 'minlplib', 'instancedata.csv')) os.rmdir(os.path.join(current_dir, 'minlplib')) def test_get_minlplib(self): current_dir = this_file_dir() - coramin.third_party.get_minlplib(download_dir=os.path.join(current_dir, 'minlplib', 'osil')) + coramin.third_party.get_minlplib( + download_dir=os.path.join(current_dir, 'minlplib', 'osil') + ) files = os.listdir(os.path.join(current_dir, 'minlplib', 'osil')) self.assertEqual(len(files), 1594) for i in files: @@ -165,7 +281,7 @@ def test_get_minlplib_problem(self): os.rmdir(os.path.join(current_dir, 'minlplib', 'gms')) os.rmdir(os.path.join(current_dir, 'minlplib')) - + class TestExceptions(unittest.TestCase): def test_exceptions1(self): current_dir = this_file_dir() @@ -187,10 +303,14 @@ def test_exceptions2(self): coramin.third_party.get_minlplib_instancedata(target_filename=filename) with self.assertRaises(ValueError): - cases = coramin.third_party.filter_minlplib_instances(instancedata_filename=filename, acceptable_probtype='foo') + cases = coramin.third_party.filter_minlplib_instances( + instancedata_filename=filename, acceptable_probtype='foo' + ) with self.assertRaises(ValueError): - cases = coramin.third_party.filter_minlplib_instances(instancedata_filename=filename, acceptable_probtype=['QCQP', 'foo']) + cases = coramin.third_party.filter_minlplib_instances( + instancedata_filename=filename, acceptable_probtype=['QCQP', 'foo'] + ) os.remove(filename) os.rmdir(os.path.dirname(filename)) @@ -199,7 +319,9 @@ def test_exceptions3(self): current_dir = this_file_dir() os.makedirs(os.path.join(current_dir, 'minlplib', 'osil')) with self.assertRaises(ValueError): - coramin.third_party.get_minlplib(download_dir=os.path.join(current_dir, 'minlplib', 'osil')) + coramin.third_party.get_minlplib( + download_dir=os.path.join(current_dir, 'minlplib', 'osil') + ) files = os.listdir(os.path.join(current_dir, 'minlplib', 'osil')) self.assertEqual(len(files), 0) os.rmdir(os.path.join(current_dir, 'minlplib', 'osil')) diff --git a/pyomo/contrib/coramin/utils/mpi_utils.py b/pyomo/contrib/coramin/utils/mpi_utils.py index f0e9c98fae6..b9dfa7ea75d 100644 --- a/pyomo/contrib/coramin/utils/mpi_utils.py +++ b/pyomo/contrib/coramin/utils/mpi_utils.py @@ -17,11 +17,11 @@ def __init__(self): @property def comm(self): return self._comm - + @property def rank(self): return self._rank - + @property def size(self): return self._size @@ -46,7 +46,7 @@ def __init__(self, mpi_interface, global_N): start = 0 end = None - for i,v in enumerate(local_N): + for i, v in enumerate(local_N): if i == self._mpi_interface.rank: end = start + v break @@ -57,21 +57,20 @@ def __init__(self, mpi_interface, global_N): def local_allocation_map(self): return list(self._local_map) - + def local_list(self, global_data): local_data = list() - assert(len(global_data) == self._global_N) + assert len(global_data) == self._global_N for i in self._local_map: local_data.append(global_data[i]) return local_data def global_list_float64(self, local_data_float64): - assert(len(local_data_float64) == len(self._local_map)) - global_data_numpy = np.zeros(self._global_N, dtype='d')*np.nan + assert len(local_data_float64) == len(self._local_map) + global_data_numpy = np.zeros(self._global_N, dtype='d') * np.nan local_data_numpy = np.asarray(local_data_float64, dtype='d') comm = self._mpi_interface.comm - comm.Allgatherv([local_data_numpy, MPI.DOUBLE], - [global_data_numpy, MPI.DOUBLE]) + comm.Allgatherv([local_data_numpy, MPI.DOUBLE], [global_data_numpy, MPI.DOUBLE]) return global_data_numpy.tolist() @@ -79,15 +78,15 @@ def global_list_float64(self, local_data_float64): def activate_mpi_printing(style='rank-0-console', rank_0_filename='output_rank_0.txt'): """ Redirect standard output based on process rank. - + Parameters ---------- style: str Can be set to one of: * 'ignore-all': ignore all printing (actually, redirect all printing to os.devnull) - * 'rank-0-console': printing from rank 0 will go to the console, + * 'rank-0-console': printing from rank 0 will go to the console, printing from other processes will be ignored - * 'rank-0-console-x-files': printing from rank 0 will go to the console, + * 'rank-0-console-x-files': printing from rank 0 will go to the console, printing from other processes will go to a separate file ('output_rank_x.txt') * 'rank-0-file': printing from rank 0 will go to 'output_rank_0.txt' * 'separate-files': printing from each processor will be redirected to a separate @@ -106,6 +105,8 @@ def activate_mpi_printing(style='rank-0-console', rank_0_filename='output_rank_0 sys.stdout = open(os.devnull, 'w') elif style == 'rank-0-console-x-files': if rank != 0: - sys.stdout = open('output_rank_{0}.txt'.format(str(MPIInterface().rank)), 'w') + sys.stdout = open( + 'output_rank_{0}.txt'.format(str(MPIInterface().rank)), 'w' + ) elif style == 'separate-files': sys.stdout = open('output_rank_{0}.txt'.format(str(MPIInterface().rank)), 'w') diff --git a/pyomo/contrib/coramin/utils/plot_relaxation.py b/pyomo/contrib/coramin/utils/plot_relaxation.py index 04d17754866..0e48f2412ff 100644 --- a/pyomo/contrib/coramin/utils/plot_relaxation.py +++ b/pyomo/contrib/coramin/utils/plot_relaxation.py @@ -1,4 +1,5 @@ import numpy as np + try: import plotly.graph_objects as go except ImportError: @@ -7,6 +8,7 @@ from .pyomo_utils import get_objective from .coramin_enums import RelaxationSide from pyomo.solvers.plugins.solvers.persistent_solver import PersistentSolver + try: import tqdm except ImportError: @@ -23,7 +25,9 @@ def _solve(m, using_persistent_solver, solver, rhs_vars, aux_var, obj): else: res = solver.solve(m, load_solutions=False) if res.solver.termination_condition != pe.TerminationCondition.optimal: - raise RuntimeError('Could not produce plot because solver did not terminate optimally') + raise RuntimeError( + 'Could not produce plot because solver did not terminate optimally' + ) if using_persistent_solver: solver.load_vars([aux_var]) else: @@ -42,8 +46,9 @@ def _solve_loop(m, x, w, x_list, using_persistent_solver, solver): res = solver.solve(m, load_solutions=False) if res.solver.termination_condition != pe.TerminationCondition.optimal: raise RuntimeError( - 'Could not produce plot because solver did not terminate optimally. Termination condition: ' + str( - res.solver.termination_condition)) + 'Could not produce plot because solver did not terminate optimally. Termination condition: ' + + str(res.solver.termination_condition) + ) if using_persistent_solver: solver.load_vars([w]) else: @@ -149,11 +154,28 @@ def sub_loop(x_ndx, _x): for y_ndx, _y in enumerate(y_list): y.fix(_y) w_true[x_ndx, y_ndx] = pe.value(rhs_expr) - if relaxation.relaxation_side in {RelaxationSide.UNDER, RelaxationSide.BOTH}: - _solve(m, using_persistent_solver, solver, rhs_vars, w, m._underestimator_obj) + if relaxation.relaxation_side in { + RelaxationSide.UNDER, + RelaxationSide.BOTH, + }: + _solve( + m, + using_persistent_solver, + solver, + rhs_vars, + w, + m._underestimator_obj, + ) w_min[x_ndx, y_ndx] = w.value if relaxation.relaxation_side in {RelaxationSide.OVER, RelaxationSide.BOTH}: - _solve(m, using_persistent_solver, solver, rhs_vars, w, m._overestimator_obj) + _solve( + m, + using_persistent_solver, + solver, + rhs_vars, + w, + m._overestimator_obj, + ) w_max[x_ndx, y_ndx] = w.value if tqdm is not None: @@ -166,9 +188,13 @@ def sub_loop(x_ndx, _x): plotly_data = list() plotly_data.append(go.Surface(x=x_list, y=y_list, z=w_true, name=str(rhs_expr))) if relaxation.relaxation_side in {RelaxationSide.UNDER, RelaxationSide.BOTH}: - plotly_data.append(go.Surface(x=x_list, y=y_list, z=w_min, name='underestimator')) + plotly_data.append( + go.Surface(x=x_list, y=y_list, z=w_min, name='underestimator') + ) if relaxation.relaxation_side in {RelaxationSide.OVER, RelaxationSide.BOTH}: - plotly_data.append(go.Surface(x=x_list, y=y_list, z=w_max, name='overestimator')) + plotly_data.append( + go.Surface(x=x_list, y=y_list, z=w_max, name='overestimator') + ) fig = go.Figure(data=plotly_data) if show_plot: @@ -193,5 +219,6 @@ def plot_relaxation(m, relaxation, solver, show_plot=True, num_pts=100): elif len(rhs_vars) == 2: _plot_3d(m, relaxation, solver, show_plot, num_pts) else: - raise NotImplementedError('Cannot generate plot for relaxation with more than 2 RHS vars') - + raise NotImplementedError( + 'Cannot generate plot for relaxation with more than 2 RHS vars' + ) diff --git a/pyomo/contrib/coramin/utils/pyomo_utils.py b/pyomo/contrib/coramin/utils/pyomo_utils.py index 6ea979cb4c7..e65774648f8 100644 --- a/pyomo/contrib/coramin/utils/pyomo_utils.py +++ b/pyomo/contrib/coramin/utils/pyomo_utils.py @@ -18,7 +18,9 @@ def get_objective(m): obj: pyomo.core.base.objective._ObjectiveData """ obj = None - for o in m.component_data_objects(pe.Objective, descend_into=True, active=True, sort=True): + for o in m.component_data_objects( + pe.Objective, descend_into=True, active=True, sort=True + ): if obj is not None: raise ValueError('Found multiple active objectives') obj = o From 35ac7d4703371caf0792ecd09be09b95cab277b3 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Fri, 27 Oct 2023 16:50:53 -0600 Subject: [PATCH 009/128] fix typos --- pyomo/contrib/coramin/relaxations/auto_relax.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/coramin/relaxations/auto_relax.py b/pyomo/contrib/coramin/relaxations/auto_relax.py index a60d61d1b16..e3de804a8cc 100644 --- a/pyomo/contrib/coramin/relaxations/auto_relax.py +++ b/pyomo/contrib/coramin/relaxations/auto_relax.py @@ -1163,7 +1163,7 @@ def _relax_root_to_leaf_GeneralExpression(node, relaxation_side_map): class _FactorableRelaxationVisitor(ExpressionValueVisitor): """ This walker generates new constraints with nonlinear terms replaced by - auxiliary variables, and relaxations relating the auxilliary variables to + auxiliary variables, and relaxations relating the auxiliary variables to the original variables. """ @@ -1508,10 +1508,10 @@ def relax( anytime the variable bounds are updated. For example, suppose the model is relaxed and, only after OBBT is performed, we find out x >= 0. We should be able to easily update the relaxation so that x**3 is then relaxed as a convex univariate function. The reason FBBT needs to be performed after relaxing the model is that - we want to make sure that all of the auxilliary variables introduced get tightened bounds. The correct way to - handle this is to perform FBBT with the original model with suspect, which forms a DAG. Each auxilliary variable + we want to make sure that all of the auxiliary variables introduced get tightened bounds. The correct way to + handle this is to perform FBBT with the original model with suspect, which forms a DAG. Each auxiliary variable introduced in the relaxed model corresponds to a node in the DAG. If we use suspect, then we can easily - update the bounds of the auxilliary variables without performing FBBT a second time. + update the bounds of the auxiliary variables without performing FBBT a second time. """ if not in_place: m = model.clone() From a54bceb3c537703669701f14fd0fd82f74538000 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Fri, 27 Oct 2023 16:55:29 -0600 Subject: [PATCH 010/128] fix typos --- pyomo/contrib/coramin/README.md | 2 +- pyomo/contrib/coramin/relaxations/relaxations_base.py | 2 +- pyomo/contrib/coramin/relaxations/segments.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/coramin/README.md b/pyomo/contrib/coramin/README.md index 1a567666b19..6b347136588 100644 --- a/pyomo/contrib/coramin/README.md +++ b/pyomo/contrib/coramin/README.md @@ -25,7 +25,7 @@ filtering techniques. ### [Carl Laird](https://github.com/carldlaird) - Parallel OBBT - McCormick and piecewise McCormick relaxations for bilinear terms -- Relaxations for univariate convex/concave fucntions +- Relaxations for univariate convex/concave functions ### [Anya Castillo](https://github.com/anyacastillo) - Relaxation classes diff --git a/pyomo/contrib/coramin/relaxations/relaxations_base.py b/pyomo/contrib/coramin/relaxations/relaxations_base.py index 8f1a75077ec..71e0f0f6707 100644 --- a/pyomo/contrib/coramin/relaxations/relaxations_base.py +++ b/pyomo/contrib/coramin/relaxations/relaxations_base.py @@ -703,7 +703,7 @@ def __init__(self, component): BaseRelaxationData.__init__(self, component) self._partitions = ComponentMap() # ComponentMap: var: list of float - self._saved_partitions = list() # list of CompnentMap + self._saved_partitions = list() # list of ComponentMap def rebuild(self, build_nonlinear_constraint=False, ensure_oa_at_vertices=True): """ diff --git a/pyomo/contrib/coramin/relaxations/segments.py b/pyomo/contrib/coramin/relaxations/segments.py index 2cd71086540..8566df30434 100644 --- a/pyomo/contrib/coramin/relaxations/segments.py +++ b/pyomo/contrib/coramin/relaxations/segments.py @@ -7,7 +7,7 @@ def compute_k_segment_points(v, k): """ - Return a list of points that generats k segments between v.lb and v.ub + Return a list of points that generates k segments between v.lb and v.ub Parameters ---------- From ccbc0b90ea2be70e8a8948efdd028a490e276cf9 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Fri, 27 Oct 2023 16:57:08 -0600 Subject: [PATCH 011/128] fix typos --- .../contrib/coramin/relaxations/univariate.py | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/pyomo/contrib/coramin/relaxations/univariate.py b/pyomo/contrib/coramin/relaxations/univariate.py index 09839640459..8c940e3f355 100644 --- a/pyomo/contrib/coramin/relaxations/univariate.py +++ b/pyomo/contrib/coramin/relaxations/univariate.py @@ -302,9 +302,9 @@ def pw_sin_relaxation( OE_tangent_intercept, ) = _compute_sine_overestimator_tangent_point(xlb) ( - UE_tangent_x, - UE_tangent_slope, - UE_tangent_intercept, + ue_tangent_x, + ue_tangent_slope, + ue_tangent_intercept, ) = _compute_sine_underestimator_tangent_point(xub) non_piecewise_overestimators_pts = [] non_piecewise_underestimator_pts = [] @@ -319,11 +319,11 @@ def pw_sin_relaxation( ) x_pts = new_x_pts elif relaxation_side == RelaxationSide.UNDER: - if UE_tangent_x > xlb: + if ue_tangent_x > xlb: new_x_pts = [xlb] - new_x_pts.extend(i for i in x_pts if i > UE_tangent_x) - non_piecewise_underestimator_pts = [i for i in x_pts if i < UE_tangent_x] - non_piecewise_underestimator_pts.append(UE_tangent_x) + new_x_pts.extend(i for i in x_pts if i > ue_tangent_x) + non_piecewise_underestimator_pts = [i for i in x_pts if i < ue_tangent_x] + non_piecewise_underestimator_pts.append(ue_tangent_x) x_pts = new_x_pts b.non_piecewise_overestimators = pyo.ConstraintList() @@ -498,9 +498,9 @@ def pw_arctan_relaxation( OE_tangent_intercept, ) = _compute_arctan_overestimator_tangent_point(xlb) ( - UE_tangent_x, - UE_tangent_slope, - UE_tangent_intercept, + ue_tangent_x, + ue_tangent_slope, + ue_tangent_intercept, ) = _compute_arctan_underestimator_tangent_point(xub) non_piecewise_overestimators_pts = [] non_piecewise_underestimator_pts = [] @@ -515,11 +515,11 @@ def pw_arctan_relaxation( ) x_pts = new_x_pts elif relaxation_side == RelaxationSide.UNDER: - if UE_tangent_x > xlb: + if ue_tangent_x > xlb: new_x_pts = [xlb] - new_x_pts.extend(i for i in x_pts if i > UE_tangent_x) - non_piecewise_underestimator_pts = [i for i in x_pts if i < UE_tangent_x] - non_piecewise_underestimator_pts.append(UE_tangent_x) + new_x_pts.extend(i for i in x_pts if i > ue_tangent_x) + non_piecewise_underestimator_pts = [i for i in x_pts if i < ue_tangent_x] + non_piecewise_underestimator_pts.append(ue_tangent_x) x_pts = new_x_pts b.non_piecewise_overestimators = pyo.ConstraintList() From 4d9775705c1a897ac36ddcc5c4c25d950b95bd7f Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Fri, 27 Oct 2023 17:03:05 -0600 Subject: [PATCH 012/128] fix typos --- pyomo/contrib/coramin/domain_reduction/dbt.py | 8 ++-- .../contrib/coramin/relaxations/mccormick.py | 4 +- .../contrib/coramin/relaxations/univariate.py | 46 ++++++++++--------- 3 files changed, 31 insertions(+), 27 deletions(-) diff --git a/pyomo/contrib/coramin/domain_reduction/dbt.py b/pyomo/contrib/coramin/domain_reduction/dbt.py index 80596eb4994..844b99a3ded 100644 --- a/pyomo/contrib/coramin/domain_reduction/dbt.py +++ b/pyomo/contrib/coramin/domain_reduction/dbt.py @@ -478,7 +478,7 @@ def _refine_partition( if not correct_structure: logger.info( - f'Constriant {str(c)} is contributing to {count} removed ' + f'Constraint {str(c)} is contributing to {count} removed ' f'edges, but we cannot split the constraint because some of ' f'the terms in the SumExpression contain variables from both ' f'partitions.' @@ -793,7 +793,7 @@ def num_cons_in_graph(graph, include_rels=True): class DecompositionStatus(enum.Enum): normal = 0 # the model was successfullay decomposed at least once and no exception was raised error = 1 # an exception was raised - bad_ratio = 2 # the model could not be decomposed at all because the min_parition_ratio was not satisfied + bad_ratio = 2 # the model could not be decomposed at all because the min_partition_ratio was not satisfied problem_too_small = 3 # the model could not be decomposed at all because the number of jacobian nonzeros in the original problem was less than max_leaf_nnz @@ -897,7 +897,7 @@ def _decompose_model( new_model: TreeBlockData The decomposed model component_map: pe.ComponentMap - A ComponentMap mapping varialbes and constraints in model to those in new_model + A ComponentMap mapping variables and constraints in model to those in new_model termination_reason: DecompositionStatus An enum member from DecompositionStatus """ @@ -1056,7 +1056,7 @@ def decompose_model( new_model: TreeBlockData The decomposed model component_map: pe.ComponentMap - A ComponentMap mapping varialbes and constraints in model to those in new_model + A ComponentMap mapping variables and constraints in model to those in new_model termination_reason: DecompositionStatus An enum member from DecompositionStatus """ diff --git a/pyomo/contrib/coramin/relaxations/mccormick.py b/pyomo/contrib/coramin/relaxations/mccormick.py index 68589c92b7f..d88396649b9 100644 --- a/pyomo/contrib/coramin/relaxations/mccormick.py +++ b/pyomo/contrib/coramin/relaxations/mccormick.py @@ -207,7 +207,7 @@ def set_input( x2 : pyomo.core.base.var._GeneralVarData The "x2" variable in x1*x2 aux_var : pyomo.core.base.var._GeneralVarData - The "aux_var" auxillary variable that is replacing x1*x2 + The "aux_var" auxiliary variable that is replacing x1*x2 relaxation_side : minlp.minlp_defn.RelaxationSide Provide the desired side for the relaxation (OVER, UNDER, or BOTH) """ @@ -242,7 +242,7 @@ def build( x2 : pyomo.core.base.var._GeneralVarData The "x2" variable in x1*x2 aux_var : pyomo.core.base.var._GeneralVarData - The "aux_var" auxillary variable that is replacing x1*x2 + The "aux_var" auxiliary variable that is replacing x1*x2 relaxation_side : minlp.minlp_defn.RelaxationSide Provide the desired side for the relaxation (OVER, UNDER, or BOTH) """ diff --git a/pyomo/contrib/coramin/relaxations/univariate.py b/pyomo/contrib/coramin/relaxations/univariate.py index 8c940e3f355..99e151eb31e 100644 --- a/pyomo/contrib/coramin/relaxations/univariate.py +++ b/pyomo/contrib/coramin/relaxations/univariate.py @@ -180,7 +180,7 @@ def _pw_univariate_relaxation( f_x_expr: pyomo expression An expression for f(x) pw_repn: str - This must be one of the valid strings for the peicewise representation to use (directly from the Piecewise + This must be one of the valid strings for the piecewise representation to use (directly from the Piecewise component). Use help(Piecewise) to learn more. shape: FunctionShape Specify the shape of the function. Valid values are minlp.FunctionShape.CONVEX or minlp.FunctionShape.CONCAVE @@ -268,7 +268,7 @@ def pw_sin_relaxation( The "x" variable in sin(x). The lower bound on x must greater than or equal to -pi/2 and the upper bound on x must be less than or equal to pi/2. w: pyomo.core.base.var.SimpleVar or pyomo.core.base.var._GeneralVarData - The auxillary variable replacing sin(x) + The auxiliary variable replacing sin(x) x_pts: Sequence[float] A list of floating point numbers to define the points over which the piecewise representation will be generated. This list must be ordered, and it is expected @@ -302,9 +302,9 @@ def pw_sin_relaxation( OE_tangent_intercept, ) = _compute_sine_overestimator_tangent_point(xlb) ( - ue_tangent_x, - ue_tangent_slope, - ue_tangent_intercept, + under_estimator_tangent_x, + under_estimator_tangent_slope, + under_estimator_tangent_intercept, ) = _compute_sine_underestimator_tangent_point(xub) non_piecewise_overestimators_pts = [] non_piecewise_underestimator_pts = [] @@ -319,11 +319,13 @@ def pw_sin_relaxation( ) x_pts = new_x_pts elif relaxation_side == RelaxationSide.UNDER: - if ue_tangent_x > xlb: + if under_estimator_tangent_x > xlb: new_x_pts = [xlb] - new_x_pts.extend(i for i in x_pts if i > ue_tangent_x) - non_piecewise_underestimator_pts = [i for i in x_pts if i < ue_tangent_x] - non_piecewise_underestimator_pts.append(ue_tangent_x) + new_x_pts.extend(i for i in x_pts if i > under_estimator_tangent_x) + non_piecewise_underestimator_pts = [ + i for i in x_pts if i < under_estimator_tangent_x + ] + non_piecewise_underestimator_pts.append(under_estimator_tangent_x) x_pts = new_x_pts b.non_piecewise_overestimators = pyo.ConstraintList() @@ -466,7 +468,7 @@ def pw_arctan_relaxation( The "x" variable in sin(x). The lower bound on x must greater than or equal to -pi/2 and the upper bound on x must be less than or equal to pi/2. w: pyomo.core.base.var.SimpleVar or pyomo.core.base.var._GeneralVarData - The auxillary variable replacing sin(x) + The auxiliary variable replacing sin(x) x_pts: Sequence[float] A list of floating point numbers to define the points over which the piecewise representation will be generated. This list must be ordered, and it is expected @@ -498,9 +500,9 @@ def pw_arctan_relaxation( OE_tangent_intercept, ) = _compute_arctan_overestimator_tangent_point(xlb) ( - ue_tangent_x, - ue_tangent_slope, - ue_tangent_intercept, + under_estimator_tangent_x, + under_estimator_tangent_slope, + under_estimator_tangent_intercept, ) = _compute_arctan_underestimator_tangent_point(xub) non_piecewise_overestimators_pts = [] non_piecewise_underestimator_pts = [] @@ -515,11 +517,13 @@ def pw_arctan_relaxation( ) x_pts = new_x_pts elif relaxation_side == RelaxationSide.UNDER: - if ue_tangent_x > xlb: + if under_estimator_tangent_x > xlb: new_x_pts = [xlb] - new_x_pts.extend(i for i in x_pts if i > ue_tangent_x) - non_piecewise_underestimator_pts = [i for i in x_pts if i < ue_tangent_x] - non_piecewise_underestimator_pts.append(ue_tangent_x) + new_x_pts.extend(i for i in x_pts if i > under_estimator_tangent_x) + non_piecewise_underestimator_pts = [ + i for i in x_pts if i < under_estimator_tangent_x + ] + non_piecewise_underestimator_pts.append(under_estimator_tangent_x) x_pts = new_x_pts b.non_piecewise_overestimators = pyo.ConstraintList() @@ -712,7 +716,7 @@ def set_input( x: pyomo.core.base.var._GeneralVarData The "x" variable in aux_var = f(x). aux_var: pyomo.core.base.var._GeneralVarData - The auxillary variable replacing f(x) + The auxiliary variable replacing f(x) shape: FunctionShape Options are FunctionShape.CONVEX and FunctionShape.CONCAVE f_x_expr: pyomo expression @@ -760,7 +764,7 @@ def build( x: pyomo.core.base.var._GeneralVarData The "x" variable in aux_var = f(x). aux_var: pyomo.core.base.var._GeneralVarData - The auxillary variable replacing f(x) + The auxiliary variable replacing f(x) shape: FunctionShape Options are FunctionShape.CONVEX and FunctionShape.CONCAVE f_x_expr: pyomo expression @@ -997,7 +1001,7 @@ def set_input( x: pyomo.core.base.var._GeneralVarData The "x" variable in aux_var = f(x). aux_var: pyomo.core.base.var._GeneralVarData - The auxillary variable replacing f(x) + The auxiliary variable replacing f(x) pw_repn: str This must be one of the valid strings for the piecewise representation to use (directly from the Piecewise component). Use help(Piecewise) to learn more. @@ -1036,7 +1040,7 @@ def build( x: pyomo.core.base.var._GeneralVarData The "x" variable in aux_var = f(x). aux_var: pyomo.core.base.var._GeneralVarData - The auxillary variable replacing f(x) + The auxiliary variable replacing f(x) pw_repn: str This must be one of the valid strings for the piecewise representation to use (directly from the Piecewise component). Use help(Piecewise) to learn more. From 8d1e68e533df01dda7ac4c4f9911db2ab388b7cf Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 8 Nov 2023 07:41:44 -0700 Subject: [PATCH 013/128] working on simplification contrib package --- pyomo/contrib/simplification/__init__.py | 0 pyomo/contrib/simplification/build.py | 33 +++++++++++++++++++ .../simplification/ginac_interface.cpp | 0 pyomo/contrib/simplification/simplify.py | 12 +++++++ 4 files changed, 45 insertions(+) create mode 100644 pyomo/contrib/simplification/__init__.py create mode 100644 pyomo/contrib/simplification/build.py create mode 100644 pyomo/contrib/simplification/ginac_interface.cpp create mode 100644 pyomo/contrib/simplification/simplify.py diff --git a/pyomo/contrib/simplification/__init__.py b/pyomo/contrib/simplification/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/pyomo/contrib/simplification/build.py b/pyomo/contrib/simplification/build.py new file mode 100644 index 00000000000..0b0b9828cd3 --- /dev/null +++ b/pyomo/contrib/simplification/build.py @@ -0,0 +1,33 @@ +from pybind11.setup_helpers import Pybind11Extension, build_ext +from pyomo.common.fileutils import this_file_dir +import os +from distutils.dist import Distribution +import sys + + +def build_ginac_interface(args=[]): + dname = this_file_dir() + _sources = [ + 'ginac_interface.cpp', + ] + sources = list() + for fname in _sources: + sources.append(os.path.join(dname, fname)) + extra_args = ['-std=c++11'] + ext = Pybind11Extension('ginac_interface', sources, extra_compile_args=extra_args) + + package_config = { + 'name': 'ginac_interface', + 'packages': [], + 'ext_modules': [ext], + 'cmdclass': {"build_ext": build_ext}, + } + + dist = Distribution(package_config) + dist.script_args = ['build_ext'] + args + dist.parse_command_line() + dist.run_command('build_ext') + + +if __name__ == '__main__': + build_ginac_interface(sys.argv[1:]) diff --git a/pyomo/contrib/simplification/ginac_interface.cpp b/pyomo/contrib/simplification/ginac_interface.cpp new file mode 100644 index 00000000000..e69de29bb2d diff --git a/pyomo/contrib/simplification/simplify.py b/pyomo/contrib/simplification/simplify.py new file mode 100644 index 00000000000..70d5dfcd9ac --- /dev/null +++ b/pyomo/contrib/simplification/simplify.py @@ -0,0 +1,12 @@ +from pyomo.core.expr.sympy_tools import sympy2pyomo_expression, sympyify_expression +from pyomo.core.expr.numeric_expr import NumericExpression +from pyomo.core.expr.numvalue import is_fixed, value + + +def simplify_with_sympy(expr: NumericExpression): + om, se = sympyify_expression(expr) + se = se.simplify() + new_expr = sympy2pyomo_expression(se, om) + if is_fixed(new_expr): + new_expr = value(new_expr) + return new_expr \ No newline at end of file From 8015d7ece05ee4608c8ddd6924218f40fd635f89 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 8 Nov 2023 11:39:43 -0700 Subject: [PATCH 014/128] working on simplification contrib package --- pyomo/contrib/simplification/build.py | 65 ++++++- .../simplification/ginac_interface.cpp | 149 ++++++++++++++++ .../simplification/ginac_interface.hpp | 165 ++++++++++++++++++ 3 files changed, 376 insertions(+), 3 deletions(-) create mode 100644 pyomo/contrib/simplification/ginac_interface.hpp diff --git a/pyomo/contrib/simplification/build.py b/pyomo/contrib/simplification/build.py index 0b0b9828cd3..6f16607e22b 100644 --- a/pyomo/contrib/simplification/build.py +++ b/pyomo/contrib/simplification/build.py @@ -1,8 +1,12 @@ from pybind11.setup_helpers import Pybind11Extension, build_ext -from pyomo.common.fileutils import this_file_dir +from pyomo.common.fileutils import this_file_dir, find_library import os from distutils.dist import Distribution import sys +import shutil +import glob +import tempfile +from pyomo.common.envvar import PYOMO_CONFIG_DIR def build_ginac_interface(args=[]): @@ -13,14 +17,69 @@ def build_ginac_interface(args=[]): sources = list() for fname in _sources: sources.append(os.path.join(dname, fname)) + + ginac_lib = find_library('ginac') + if ginac_lib is None: + raise RuntimeError('could not find GiNaC library; please make sure it is in the LD_LIBRARY_PATH environment variable') + ginac_lib_dir = os.path.dirname(ginac_lib) + ginac_build_dir = os.path.dirname(ginac_lib_dir) + ginac_include_dir = os.path.join(ginac_build_dir, 'include') + if not os.path.exists(os.path.join(ginac_include_dir, 'ginac', 'ginac.h')): + raise RuntimeError('could not find GiNaC include directory') + + cln_lib = find_library('cln') + if cln_lib is None: + raise RuntimeError('could not find CLN library; please make sure it is in the LD_LIBRARY_PATH environment variable') + cln_lib_dir = os.path.dirname(cln_lib) + cln_build_dir = os.path.dirname(cln_lib_dir) + cln_include_dir = os.path.join(cln_build_dir, 'include') + if not os.path.exists(os.path.join(cln_include_dir, 'cln', 'cln.h')): + raise RuntimeError('could not find CLN include directory') + extra_args = ['-std=c++11'] - ext = Pybind11Extension('ginac_interface', sources, extra_compile_args=extra_args) + ext = Pybind11Extension( + 'ginac_interface', + sources=sources, + language='c++', + include_dirs=[cln_include_dir, ginac_include_dir], + library_dirs=[cln_lib_dir, ginac_lib_dir], + libraries=['cln', 'ginac'], + extra_compile_args=extra_args, + ) + + class ginac_build_ext(build_ext): + def run(self): + basedir = os.path.abspath(os.path.curdir) + if self.inplace: + tmpdir = this_file_dir() + else: + tmpdir = os.path.abspath(tempfile.mkdtemp()) + print("Building in '%s'" % tmpdir) + os.chdir(tmpdir) + try: + super(ginac_build_ext, self).run() + if not self.inplace: + library = glob.glob("build/*/ginac_interface.*")[0] + target = os.path.join( + PYOMO_CONFIG_DIR, + 'lib', + 'python%s.%s' % sys.version_info[:2], + 'site-packages', + '.', + ) + if not os.path.exists(target): + os.makedirs(target) + shutil.copy(library, target) + finally: + os.chdir(basedir) + if not self.inplace: + shutil.rmtree(tmpdir, onerror=handleReadonly) package_config = { 'name': 'ginac_interface', 'packages': [], 'ext_modules': [ext], - 'cmdclass': {"build_ext": build_ext}, + 'cmdclass': {"build_ext": ginac_build_ext}, } dist = Distribution(package_config) diff --git a/pyomo/contrib/simplification/ginac_interface.cpp b/pyomo/contrib/simplification/ginac_interface.cpp index e69de29bb2d..ccbc98d3586 100644 --- a/pyomo/contrib/simplification/ginac_interface.cpp +++ b/pyomo/contrib/simplification/ginac_interface.cpp @@ -0,0 +1,149 @@ +#include "ginac_interface.hpp" + +ex ginac_expr_from_pyomo_node(py::handle expr, std::unordered_map &leaf_map, PyomoExprTypes &expr_types) { + ex res; + ExprType tmp_type = + expr_types.expr_type_map[py::type::of(expr)].cast(); + + switch (tmp_type) { + case py_float: { + res = numeric(expr.cast()); + break; + } + case var: { + long expr_id = expr_types.id(expr).cast(); + if (leaf_map.count(expr_id) == 0) { + leaf_map[expr_id] = symbol("x" + std::to_string(expr_id)); + } + res = leaf_map[expr_id]; + break; + } + case param: { + long expr_id = expr_types.id(expr).cast(); + if (leaf_map.count(expr_id) == 0) { + leaf_map[expr_id] = symbol("p" + std::to_string(expr_id)); + } + res = leaf_map[expr_id]; + break; + } + case product: { + py::list pyomo_args = expr.attr("args"); + res = ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types) * ginac_expr_from_pyomo_node(pyomo_args[1], leaf_map, expr_types); + break; + } + case sum: { + py::list pyomo_args = expr.attr("args"); + for (py::handle arg : pyomo_args) { + res += ginac_expr_from_pyomo_node(arg, leaf_map, expr_types); + } + break; + } + case negation: { + py::list pyomo_args = expr.attr("args"); + res = - ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types); + break; + } + case external_func: { + long expr_id = expr_types.id(expr).cast(); + if (leaf_map.count(expr_id) == 0) { + leaf_map[expr_id] = symbol("f" + std::to_string(expr_id)); + } + res = leaf_map[expr_id]; + break; + } + case ExprType::power: { + py::list pyomo_args = expr.attr("args"); + res = pow(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types), ginac_expr_from_pyomo_node(pyomo_args[1], leaf_map, expr_types)); + break; + } + case division: { + py::list pyomo_args = expr.attr("args"); + res = ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types) / ginac_expr_from_pyomo_node(pyomo_args[1], leaf_map, expr_types); + break; + } + case unary_func: { + std::string function_name = expr.attr("getname")().cast(); + py::list pyomo_args = expr.attr("args"); + if (function_name == "exp") + res = exp(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types)); + else if (function_name == "log") + res = log(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types)); + else if (function_name == "sin") + res = sin(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types)); + else if (function_name == "cos") + res = cos(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types)); + else if (function_name == "tan") + res = tan(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types)); + else if (function_name == "asin") + res = asin(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types)); + else if (function_name == "acos") + res = acos(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types)); + else if (function_name == "atan") + res = atan(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types)); + else if (function_name == "sqrt") + res = sqrt(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types)); + else + throw py::value_error("Unrecognized expression type: " + function_name); + break; + } + case linear: { + py::list pyomo_args = expr.attr("args"); + for (py::handle arg : pyomo_args) { + res += ginac_expr_from_pyomo_node(arg, leaf_map, expr_types); + } + break; + } + case named_expr: { + res = ginac_expr_from_pyomo_node(expr.attr("expr"), leaf_map, expr_types); + break; + } + case numeric_constant: { + res = numeric(expr.attr("value").cast()); + break; + } + case pyomo_unit: { + res = numeric(1.0); + break; + } + case unary_abs: { + py::list pyomo_args = expr.attr("args"); + res = abs(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types)); + break; + } + default: { + throw py::value_error("Unrecognized expression type: " + + expr_types.builtins.attr("str")(py::type::of(expr)) + .cast()); + break; + } + } + return res; +} + +ex ginac_expr_from_pyomo_expr(py::handle expr, PyomoExprTypes &expr_types) { + std::unordered_map leaf_map; + ex res = ginac_expr_from_pyomo_node(expr, leaf_map, expr_types); + return res; +} + + +PYBIND11_MODULE(ginac_interface, m) { + m.def("ginac_expr_from_pyomo_expr", &ginac_expr_from_pyomo_expr); + py::class_(m, "PyomoExprTypes").def(py::init<>()); + py::class_(m, "ex"); + py::enum_(m, "ExprType") + .value("py_float", ExprType::py_float) + .value("var", ExprType::var) + .value("param", ExprType::param) + .value("product", ExprType::product) + .value("sum", ExprType::sum) + .value("negation", ExprType::negation) + .value("external_func", ExprType::external_func) + .value("power", ExprType::power) + .value("division", ExprType::division) + .value("unary_func", ExprType::unary_func) + .value("linear", ExprType::linear) + .value("named_expr", ExprType::named_expr) + .value("numeric_constant", ExprType::numeric_constant) + .export_values(); +} diff --git a/pyomo/contrib/simplification/ginac_interface.hpp b/pyomo/contrib/simplification/ginac_interface.hpp new file mode 100644 index 00000000000..de77e66d0c7 --- /dev/null +++ b/pyomo/contrib/simplification/ginac_interface.hpp @@ -0,0 +1,165 @@ +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#define PYBIND11_DETAILED_ERROR_MESSAGES + +namespace py = pybind11; +using namespace pybind11::literals; +using namespace GiNaC; + +enum ExprType { + py_float = 0, + var = 1, + param = 2, + product = 3, + sum = 4, + negation = 5, + external_func = 6, + power = 7, + division = 8, + unary_func = 9, + linear = 10, + named_expr = 11, + numeric_constant = 12, + pyomo_unit = 13, + unary_abs = 14 +}; + +class PyomoExprTypes { +public: + PyomoExprTypes() { + expr_type_map[int_] = py_float; + expr_type_map[float_] = py_float; + expr_type_map[np_int16] = py_float; + expr_type_map[np_int32] = py_float; + expr_type_map[np_int64] = py_float; + expr_type_map[np_longlong] = py_float; + expr_type_map[np_uint16] = py_float; + expr_type_map[np_uint32] = py_float; + expr_type_map[np_uint64] = py_float; + expr_type_map[np_ulonglong] = py_float; + expr_type_map[np_float16] = py_float; + expr_type_map[np_float32] = py_float; + expr_type_map[np_float64] = py_float; + expr_type_map[ScalarVar] = var; + expr_type_map[_GeneralVarData] = var; + expr_type_map[AutoLinkedBinaryVar] = var; + expr_type_map[ScalarParam] = param; + expr_type_map[_ParamData] = param; + expr_type_map[MonomialTermExpression] = product; + expr_type_map[ProductExpression] = product; + expr_type_map[NPV_ProductExpression] = product; + expr_type_map[SumExpression] = sum; + expr_type_map[NPV_SumExpression] = sum; + expr_type_map[NegationExpression] = negation; + expr_type_map[NPV_NegationExpression] = negation; + expr_type_map[ExternalFunctionExpression] = external_func; + expr_type_map[NPV_ExternalFunctionExpression] = external_func; + expr_type_map[PowExpression] = ExprType::power; + expr_type_map[NPV_PowExpression] = ExprType::power; + expr_type_map[DivisionExpression] = division; + expr_type_map[NPV_DivisionExpression] = division; + expr_type_map[UnaryFunctionExpression] = unary_func; + expr_type_map[NPV_UnaryFunctionExpression] = unary_func; + expr_type_map[LinearExpression] = linear; + expr_type_map[_GeneralExpressionData] = named_expr; + expr_type_map[ScalarExpression] = named_expr; + expr_type_map[Integral] = named_expr; + expr_type_map[ScalarIntegral] = named_expr; + expr_type_map[NumericConstant] = numeric_constant; + expr_type_map[_PyomoUnit] = pyomo_unit; + expr_type_map[AbsExpression] = unary_abs; + expr_type_map[NPV_AbsExpression] = unary_abs; + } + ~PyomoExprTypes() = default; + py::int_ ione = 1; + py::float_ fone = 1.0; + py::type int_ = py::type::of(ione); + py::type float_ = py::type::of(fone); + py::object np = py::module_::import("numpy"); + py::type np_int16 = np.attr("int16"); + py::type np_int32 = np.attr("int32"); + py::type np_int64 = np.attr("int64"); + py::type np_longlong = np.attr("longlong"); + py::type np_uint16 = np.attr("uint16"); + py::type np_uint32 = np.attr("uint32"); + py::type np_uint64 = np.attr("uint64"); + py::type np_ulonglong = np.attr("ulonglong"); + py::type np_float16 = np.attr("float16"); + py::type np_float32 = np.attr("float32"); + py::type np_float64 = np.attr("float64"); + py::object ScalarParam = + py::module_::import("pyomo.core.base.param").attr("ScalarParam"); + py::object _ParamData = + py::module_::import("pyomo.core.base.param").attr("_ParamData"); + py::object ScalarVar = + py::module_::import("pyomo.core.base.var").attr("ScalarVar"); + py::object _GeneralVarData = + py::module_::import("pyomo.core.base.var").attr("_GeneralVarData"); + py::object AutoLinkedBinaryVar = + py::module_::import("pyomo.gdp.disjunct").attr("AutoLinkedBinaryVar"); + py::object numeric_expr = py::module_::import("pyomo.core.expr.numeric_expr"); + py::object NegationExpression = numeric_expr.attr("NegationExpression"); + py::object NPV_NegationExpression = + numeric_expr.attr("NPV_NegationExpression"); + py::object ExternalFunctionExpression = + numeric_expr.attr("ExternalFunctionExpression"); + py::object NPV_ExternalFunctionExpression = + numeric_expr.attr("NPV_ExternalFunctionExpression"); + py::object PowExpression = numeric_expr.attr("PowExpression"); + py::object NPV_PowExpression = numeric_expr.attr("NPV_PowExpression"); + py::object ProductExpression = numeric_expr.attr("ProductExpression"); + py::object NPV_ProductExpression = numeric_expr.attr("NPV_ProductExpression"); + py::object MonomialTermExpression = + numeric_expr.attr("MonomialTermExpression"); + py::object DivisionExpression = numeric_expr.attr("DivisionExpression"); + py::object NPV_DivisionExpression = + numeric_expr.attr("NPV_DivisionExpression"); + py::object SumExpression = numeric_expr.attr("SumExpression"); + py::object NPV_SumExpression = numeric_expr.attr("NPV_SumExpression"); + py::object UnaryFunctionExpression = + numeric_expr.attr("UnaryFunctionExpression"); + py::object AbsExpression = numeric_expr.attr("AbsExpression"); + py::object NPV_AbsExpression = numeric_expr.attr("NPV_AbsExpression"); + py::object NPV_UnaryFunctionExpression = + numeric_expr.attr("NPV_UnaryFunctionExpression"); + py::object LinearExpression = numeric_expr.attr("LinearExpression"); + py::object NumericConstant = + py::module_::import("pyomo.core.expr.numvalue").attr("NumericConstant"); + py::object expr_module = py::module_::import("pyomo.core.base.expression"); + py::object _GeneralExpressionData = + expr_module.attr("_GeneralExpressionData"); + py::object ScalarExpression = expr_module.attr("ScalarExpression"); + py::object ScalarIntegral = + py::module_::import("pyomo.dae.integral").attr("ScalarIntegral"); + py::object Integral = + py::module_::import("pyomo.dae.integral").attr("Integral"); + py::object _PyomoUnit = + py::module_::import("pyomo.core.base.units_container").attr("_PyomoUnit"); + py::object builtins = py::module_::import("builtins"); + py::object id = builtins.attr("id"); + py::object len = builtins.attr("len"); + py::dict expr_type_map; +}; + +ex ginac_expr_from_pyomo_expr(py::handle expr, PyomoExprTypes &expr_types); From 56e8ac84e72bbd59d57e6307ef62b6dbabe09e37 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 20 Nov 2023 11:43:42 -0700 Subject: [PATCH 015/128] working on ginac interface for simplification --- .../simplification/ginac_interface.cpp | 213 ++++++++++++++++-- .../simplification/ginac_interface.hpp | 27 ++- 2 files changed, 214 insertions(+), 26 deletions(-) diff --git a/pyomo/contrib/simplification/ginac_interface.cpp b/pyomo/contrib/simplification/ginac_interface.cpp index ccbc98d3586..9a84521ff91 100644 --- a/pyomo/contrib/simplification/ginac_interface.cpp +++ b/pyomo/contrib/simplification/ginac_interface.cpp @@ -1,6 +1,12 @@ #include "ginac_interface.hpp" -ex ginac_expr_from_pyomo_node(py::handle expr, std::unordered_map &leaf_map, PyomoExprTypes &expr_types) { +ex ginac_expr_from_pyomo_node( + py::handle expr, + std::unordered_map &leaf_map, + std::unordered_map &ginac_pyomo_map, + PyomoExprTypes &expr_types, + bool symbolic_solver_labels + ) { ex res; ExprType tmp_type = expr_types.expr_type_map[py::type::of(expr)].cast(); @@ -13,7 +19,21 @@ ex ginac_expr_from_pyomo_node(py::handle expr, std::unordered_map &lea case var: { long expr_id = expr_types.id(expr).cast(); if (leaf_map.count(expr_id) == 0) { - leaf_map[expr_id] = symbol("x" + std::to_string(expr_id)); + std::string vname; + if (symbolic_solver_labels) { + vname = expr.attr("name").cast(); + } + else { + vname = "x" + std::to_string(expr_id); + } + py::object lb = expr.attr("lb"); + if (lb.is_none() || lb.cast() < 0) { + leaf_map[expr_id] = realsymbol(vname); + } + else { + leaf_map[expr_id] = possymbol(vname); + } + ginac_pyomo_map[leaf_map[expr_id]] = expr.cast(); } res = leaf_map[expr_id]; break; @@ -21,67 +41,76 @@ ex ginac_expr_from_pyomo_node(py::handle expr, std::unordered_map &lea case param: { long expr_id = expr_types.id(expr).cast(); if (leaf_map.count(expr_id) == 0) { - leaf_map[expr_id] = symbol("p" + std::to_string(expr_id)); + std::string pname; + if (symbolic_solver_labels) { + pname = expr.attr("name").cast(); + } + else { + pname = "p" + std::to_string(expr_id); + } + leaf_map[expr_id] = realsymbol(pname); + ginac_pyomo_map[leaf_map[expr_id]] = expr.cast(); } res = leaf_map[expr_id]; break; } case product: { py::list pyomo_args = expr.attr("args"); - res = ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types) * ginac_expr_from_pyomo_node(pyomo_args[1], leaf_map, expr_types); + res = ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels) * ginac_expr_from_pyomo_node(pyomo_args[1], leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels); break; } case sum: { py::list pyomo_args = expr.attr("args"); for (py::handle arg : pyomo_args) { - res += ginac_expr_from_pyomo_node(arg, leaf_map, expr_types); + res += ginac_expr_from_pyomo_node(arg, leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels); } break; } case negation: { py::list pyomo_args = expr.attr("args"); - res = - ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types); + res = - ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels); break; } case external_func: { long expr_id = expr_types.id(expr).cast(); if (leaf_map.count(expr_id) == 0) { - leaf_map[expr_id] = symbol("f" + std::to_string(expr_id)); + leaf_map[expr_id] = realsymbol("f" + std::to_string(expr_id)); + ginac_pyomo_map[leaf_map[expr_id]] = expr.cast(); } res = leaf_map[expr_id]; break; } case ExprType::power: { py::list pyomo_args = expr.attr("args"); - res = pow(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types), ginac_expr_from_pyomo_node(pyomo_args[1], leaf_map, expr_types)); + res = pow(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels), ginac_expr_from_pyomo_node(pyomo_args[1], leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels)); break; } case division: { py::list pyomo_args = expr.attr("args"); - res = ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types) / ginac_expr_from_pyomo_node(pyomo_args[1], leaf_map, expr_types); + res = ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels) / ginac_expr_from_pyomo_node(pyomo_args[1], leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels); break; } case unary_func: { std::string function_name = expr.attr("getname")().cast(); py::list pyomo_args = expr.attr("args"); if (function_name == "exp") - res = exp(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types)); + res = exp(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels)); else if (function_name == "log") - res = log(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types)); + res = log(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels)); else if (function_name == "sin") - res = sin(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types)); + res = sin(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels)); else if (function_name == "cos") - res = cos(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types)); + res = cos(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels)); else if (function_name == "tan") - res = tan(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types)); + res = tan(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels)); else if (function_name == "asin") - res = asin(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types)); + res = asin(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels)); else if (function_name == "acos") - res = acos(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types)); + res = acos(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels)); else if (function_name == "atan") - res = atan(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types)); + res = atan(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels)); else if (function_name == "sqrt") - res = sqrt(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types)); + res = sqrt(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels)); else throw py::value_error("Unrecognized expression type: " + function_name); break; @@ -89,12 +118,12 @@ ex ginac_expr_from_pyomo_node(py::handle expr, std::unordered_map &lea case linear: { py::list pyomo_args = expr.attr("args"); for (py::handle arg : pyomo_args) { - res += ginac_expr_from_pyomo_node(arg, leaf_map, expr_types); + res += ginac_expr_from_pyomo_node(arg, leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels); } break; } case named_expr: { - res = ginac_expr_from_pyomo_node(expr.attr("expr"), leaf_map, expr_types); + res = ginac_expr_from_pyomo_node(expr.attr("expr"), leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels); break; } case numeric_constant: { @@ -107,7 +136,7 @@ ex ginac_expr_from_pyomo_node(py::handle expr, std::unordered_map &lea } case unary_abs: { py::list pyomo_args = expr.attr("args"); - res = abs(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, expr_types)); + res = abs(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels)); break; } default: { @@ -120,17 +149,151 @@ ex ginac_expr_from_pyomo_node(py::handle expr, std::unordered_map &lea return res; } -ex ginac_expr_from_pyomo_expr(py::handle expr, PyomoExprTypes &expr_types) { +ex pyomo_expr_to_ginac_expr( + py::handle expr, + std::unordered_map &leaf_map, + std::unordered_map &ginac_pyomo_map, + PyomoExprTypes &expr_types, + bool symbolic_solver_labels + ) { + ex res = ginac_expr_from_pyomo_node(expr, leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels); + return res; + } + +ex pyomo_to_ginac(py::handle expr, PyomoExprTypes &expr_types) { std::unordered_map leaf_map; - ex res = ginac_expr_from_pyomo_node(expr, leaf_map, expr_types); + std::unordered_map ginac_pyomo_map; + ex res = ginac_expr_from_pyomo_node(expr, leaf_map, ginac_pyomo_map, expr_types, true); return res; } +class GinacToPyomoVisitor +: public visitor, + public symbol::visitor, + public numeric::visitor, + public add::visitor, + public mul::visitor, + public GiNaC::power::visitor, + public function::visitor, + public basic::visitor +{ + public: + std::unordered_map *leaf_map; + std::unordered_map node_map; + PyomoExprTypes *expr_types; + + GinacToPyomoVisitor(std::unordered_map *_leaf_map, PyomoExprTypes *_expr_types) : leaf_map(_leaf_map), expr_types(_expr_types) {} + ~GinacToPyomoVisitor() = default; + + void visit(const symbol& e) { + node_map[e] = leaf_map->at(e); + } + + void visit(const numeric& e) { + double val = e.to_double(); + node_map[e] = expr_types->NumericConstant(py::cast(val)); + } + + void visit(const add& e) { + size_t n = e.nops(); + py::object pe = node_map[e.op(0)]; + for (unsigned long ndx=1; ndx < n; ++ndx) { + pe = pe.attr("__add__")(node_map[e.op(ndx)]); + } + node_map[e] = pe; + } + + void visit(const mul& e) { + size_t n = e.nops(); + py::object pe = node_map[e.op(0)]; + for (unsigned long ndx=1; ndx < n; ++ndx) { + pe = pe.attr("__mul__")(node_map[e.op(ndx)]); + } + node_map[e] = pe; + } + + void visit(const GiNaC::power& e) { + py::object arg1 = node_map[e.op(0)]; + py::object arg2 = node_map[e.op(1)]; + py::object pe = arg1.attr("__pow__")(arg2); + node_map[e] = pe; + } + + void visit(const function& e) { + py::object arg = node_map[e.op(0)]; + std::string func_type = e.get_name(); + py::object pe; + if (func_type == "exp") { + pe = expr_types->exp(arg); + } + else if (func_type == "log") { + pe = expr_types->log(arg); + } + else if (func_type == "sin") { + pe = expr_types->sin(arg); + } + else if (func_type == "cos") { + pe = expr_types->cos(arg); + } + else if (func_type == "tan") { + pe = expr_types->tan(arg); + } + else if (func_type == "asin") { + pe = expr_types->asin(arg); + } + else if (func_type == "acos") { + pe = expr_types->acos(arg); + } + else if (func_type == "atan") { + pe = expr_types->atan(arg); + } + else if (func_type == "sqrt") { + pe = expr_types->sqrt(arg); + } + else { + throw py::value_error("unrecognized unary function: " + func_type); + } + node_map[e] = pe; + } + + void visit(const basic& e) { + throw py::value_error("unrecognized ginac expression type"); + } +}; + + +ex GinacInterface::to_ginac(py::handle expr) { + return pyomo_expr_to_ginac_expr(expr, leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels); +} + +py::object GinacInterface::from_ginac(ex &ge) { + GinacToPyomoVisitor v(&ginac_pyomo_map, &expr_types); + ge.traverse_postorder(v); + return v.node_map[ge]; +} + PYBIND11_MODULE(ginac_interface, m) { - m.def("ginac_expr_from_pyomo_expr", &ginac_expr_from_pyomo_expr); + m.def("pyomo_to_ginac", &pyomo_to_ginac); py::class_(m, "PyomoExprTypes").def(py::init<>()); - py::class_(m, "ex"); + py::class_(m, "ginac_expression") + .def("expand", [](ex &ge) { + // exmap m; + // ex q; + // q = ge.to_polynomial(m).normal(); + // return q.subs(m); + // return factor(ge.normal()); + return ge.expand(); + }) + .def("__str__", [](ex &ge) { + std::ostringstream stream; + stream << ge; + return stream.str(); + }); + py::class_(m, "GinacInterface") + .def(py::init()) + .def("to_ginac", &GinacInterface::to_ginac) + .def("from_ginac", &GinacInterface::from_ginac); py::enum_(m, "ExprType") .value("py_float", ExprType::py_float) .value("var", ExprType::var) diff --git a/pyomo/contrib/simplification/ginac_interface.hpp b/pyomo/contrib/simplification/ginac_interface.hpp index de77e66d0c7..bc5b0d7b6fc 100644 --- a/pyomo/contrib/simplification/ginac_interface.hpp +++ b/pyomo/contrib/simplification/ginac_interface.hpp @@ -156,10 +156,35 @@ class PyomoExprTypes { py::module_::import("pyomo.dae.integral").attr("Integral"); py::object _PyomoUnit = py::module_::import("pyomo.core.base.units_container").attr("_PyomoUnit"); + py::object exp = numeric_expr.attr("exp"); + py::object log = numeric_expr.attr("log"); + py::object sin = numeric_expr.attr("sin"); + py::object cos = numeric_expr.attr("cos"); + py::object tan = numeric_expr.attr("tan"); + py::object asin = numeric_expr.attr("asin"); + py::object acos = numeric_expr.attr("acos"); + py::object atan = numeric_expr.attr("atan"); + py::object sqrt = numeric_expr.attr("sqrt"); py::object builtins = py::module_::import("builtins"); py::object id = builtins.attr("id"); py::object len = builtins.attr("len"); py::dict expr_type_map; }; -ex ginac_expr_from_pyomo_expr(py::handle expr, PyomoExprTypes &expr_types); +ex pyomo_to_ginac(py::handle expr, PyomoExprTypes &expr_types); + + +class GinacInterface { + public: + std::unordered_map leaf_map; + std::unordered_map ginac_pyomo_map; + PyomoExprTypes expr_types; + bool symbolic_solver_labels = false; + + GinacInterface() = default; + GinacInterface(bool _symbolic_solver_labels) : symbolic_solver_labels(_symbolic_solver_labels) {} + ~GinacInterface() = default; + + ex to_ginac(py::handle expr); + py::object from_ginac(ex &ginac_expr); +}; From 932d3d6a8a7a1cd95f2e227fe9f3a63849ad02a4 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 20 Nov 2023 13:07:37 -0700 Subject: [PATCH 016/128] ginac interface improvements --- .../simplification/ginac_interface.cpp | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/pyomo/contrib/simplification/ginac_interface.cpp b/pyomo/contrib/simplification/ginac_interface.cpp index 9a84521ff91..690885dc513 100644 --- a/pyomo/contrib/simplification/ginac_interface.cpp +++ b/pyomo/contrib/simplification/ginac_interface.cpp @@ -1,5 +1,11 @@ #include "ginac_interface.hpp" + +bool is_integer(double x) { + return std::floor(x) == x; +} + + ex ginac_expr_from_pyomo_node( py::handle expr, std::unordered_map &leaf_map, @@ -13,7 +19,13 @@ ex ginac_expr_from_pyomo_node( switch (tmp_type) { case py_float: { - res = numeric(expr.cast()); + double val = expr.cast(); + if (is_integer(val)) { + res = numeric(expr.cast()); + } + else { + res = numeric(val); + } break; } case var: { @@ -278,13 +290,9 @@ PYBIND11_MODULE(ginac_interface, m) { py::class_(m, "PyomoExprTypes").def(py::init<>()); py::class_(m, "ginac_expression") .def("expand", [](ex &ge) { - // exmap m; - // ex q; - // q = ge.to_polynomial(m).normal(); - // return q.subs(m); - // return factor(ge.normal()); return ge.expand(); }) + .def("normal", &ex::normal) .def("__str__", [](ex &ge) { std::ostringstream stream; stream << ge; From 208a5dac01ca7dab7830f70119eeb0e1ba94918e Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 20 Nov 2023 13:27:31 -0700 Subject: [PATCH 017/128] simplification interface --- pyomo/contrib/simplification/simplify.py | 35 +++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/simplification/simplify.py b/pyomo/contrib/simplification/simplify.py index 70d5dfcd9ac..1de228fb444 100644 --- a/pyomo/contrib/simplification/simplify.py +++ b/pyomo/contrib/simplification/simplify.py @@ -1,6 +1,17 @@ from pyomo.core.expr.sympy_tools import sympy2pyomo_expression, sympyify_expression from pyomo.core.expr.numeric_expr import NumericExpression from pyomo.core.expr.numvalue import is_fixed, value +import logging +import warnings +try: + from pyomo.contrib.simplification.ginac_interface import GinacInterface + ginac_available = True +except: + GinacInterface = None + ginac_available = False + + +logger = logging.getLogger(__name__) def simplify_with_sympy(expr: NumericExpression): @@ -9,4 +20,26 @@ def simplify_with_sympy(expr: NumericExpression): new_expr = sympy2pyomo_expression(se, om) if is_fixed(new_expr): new_expr = value(new_expr) - return new_expr \ No newline at end of file + return new_expr + + +def simplify_with_ginac(expr: NumericExpression, ginac_interface): + gi = ginac_interface + return gi.from_ginac(gi.to_ginac(expr).normal()) + + +class Simplifier(object): + def __init__(self, supress_no_ginac_warnings: bool = False) -> None: + if ginac_available: + self.gi = GinacInterface() + self.suppress_no_ginac_warnings = supress_no_ginac_warnings + + def simplify(self, expr: NumericExpression): + if ginac_available: + return simplify_with_ginac(expr, self.gi) + else: + if not self.suppress_no_ginac_warnings: + msg = f"GiNaC does not seem to be available. Using SymPy. Note that the GiNac interface is significantly faster." + logger.warning(msg) + warnings.warn(msg) + return simplify_with_sympy(expr) From 6ef89144c5b088574736e86553cb8c22d2dd9645 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 20 Nov 2023 21:08:25 -0700 Subject: [PATCH 018/128] bugs --- pyomo/contrib/simplification/__init__.py | 1 + pyomo/contrib/simplification/simplify.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/simplification/__init__.py b/pyomo/contrib/simplification/__init__.py index e69de29bb2d..c09e8b8b5e5 100644 --- a/pyomo/contrib/simplification/__init__.py +++ b/pyomo/contrib/simplification/__init__.py @@ -0,0 +1 @@ +from .simplify import Simplifier \ No newline at end of file diff --git a/pyomo/contrib/simplification/simplify.py b/pyomo/contrib/simplification/simplify.py index 1de228fb444..938bff6b4b9 100644 --- a/pyomo/contrib/simplification/simplify.py +++ b/pyomo/contrib/simplification/simplify.py @@ -31,7 +31,7 @@ def simplify_with_ginac(expr: NumericExpression, ginac_interface): class Simplifier(object): def __init__(self, supress_no_ginac_warnings: bool = False) -> None: if ginac_available: - self.gi = GinacInterface() + self.gi = GinacInterface(False) self.suppress_no_ginac_warnings = supress_no_ginac_warnings def simplify(self, expr: NumericExpression): From af47ef791888b6327c295a06544c4c0771e712d9 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 21 Nov 2023 00:48:31 -0700 Subject: [PATCH 019/128] simplification tests --- .../contrib/simplification/tests/__init__.py | 0 .../tests/test_simplification.py | 62 +++++++++++++++++++ pyomo/core/expr/compare.py | 14 ++++- 3 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 pyomo/contrib/simplification/tests/__init__.py create mode 100644 pyomo/contrib/simplification/tests/test_simplification.py diff --git a/pyomo/contrib/simplification/tests/__init__.py b/pyomo/contrib/simplification/tests/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/pyomo/contrib/simplification/tests/test_simplification.py b/pyomo/contrib/simplification/tests/test_simplification.py new file mode 100644 index 00000000000..02107ba1d6c --- /dev/null +++ b/pyomo/contrib/simplification/tests/test_simplification.py @@ -0,0 +1,62 @@ +from pyomo.common.unittest import TestCase +from pyomo.contrib.simplification import Simplifier +from pyomo.core.expr.compare import assertExpressionsEqual, compare_expressions +import pyomo.environ as pe +from pyomo.core.expr.calculus.diff_with_pyomo import reverse_sd + + +class TestSimplification(TestCase): + def test_simplify(self): + m = pe.ConcreteModel() + x = m.x = pe.Var(bounds=(0, None)) + e = x*pe.log(x) + der1 = reverse_sd(e)[x] + der2 = reverse_sd(der1)[x] + simp = Simplifier() + der2_simp = simp.simplify(der2) + expected = x**-1.0 + assertExpressionsEqual(self, expected, der2_simp) + + def test_param(self): + m = pe.ConcreteModel() + x = m.x = pe.Var() + p = m.p = pe.Param(mutable=True) + e1 = p*x**2 + p*x + p*x**2 + simp = Simplifier() + e2 = simp.simplify(e1) + exp1 = p*x**2.0*2.0 + p*x + exp2 = p*x + p*x**2.0*2.0 + self.assertTrue( + compare_expressions(e2, exp1) + or compare_expressions(e2, exp2) + or compare_expressions(e2, p*x + x**2.0*p*2.0) + or compare_expressions(e2, x**2.0*p*2.0 + p*x) + ) + + def test_mul(self): + m = pe.ConcreteModel() + x = m.x = pe.Var() + e = 2*x + simp = Simplifier() + e2 = simp.simplify(e) + expected = 2.0*x + assertExpressionsEqual(self, expected, e2) + + def test_sum(self): + m = pe.ConcreteModel() + x = m.x = pe.Var() + e = 2 + x + simp = Simplifier() + e2 = simp.simplify(e) + expected = x + 2.0 + assertExpressionsEqual(self, expected, e2) + + def test_neg(self): + m = pe.ConcreteModel() + x = m.x = pe.Var() + e = -pe.log(x) + simp = Simplifier() + e2 = simp.simplify(e) + expected = pe.log(x)*(-1.0) + assertExpressionsEqual(self, expected, e2) + diff --git a/pyomo/core/expr/compare.py b/pyomo/core/expr/compare.py index ec8d56896b8..96913f1de39 100644 --- a/pyomo/core/expr/compare.py +++ b/pyomo/core/expr/compare.py @@ -195,7 +195,19 @@ def compare_expressions(expr1, expr2, include_named_exprs=True): expr2, include_named_exprs=include_named_exprs ) try: - res = pn1 == pn2 + res = True + if len(pn1) != len(pn2): + res = False + if res: + for a, b in zip(pn1, pn2): + if a.__class__ is not b.__class__: + res = False + break + if a == b: + continue + else: + res = False + break except PyomoException: res = False return res From a6f0e2af6e25f6fa3d8cc605764f274e079bbdbd Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Thu, 21 Dec 2023 23:18:09 -0700 Subject: [PATCH 020/128] coramin improvements --- pyomo/contrib/appsi/fbbt.py | 6 +- pyomo/contrib/coramin/algorithms/bnb/bnb.py | 116 ++++++++++ .../coramin/cutting_planes/__init__.py | 0 .../coramin/cutting_planes/alpha_bb_cuts.py | 36 ++++ pyomo/contrib/coramin/cutting_planes/base.py | 10 + pyomo/contrib/coramin/heuristics/__init__.py | 0 pyomo/contrib/coramin/heuristics/diving.py | 164 +++++++++++++++ .../coramin/heuristics/feasibility_pump.py | 198 ++++++++++++++++++ pyomo/contrib/fbbt/interval.py | 11 +- 9 files changed, 538 insertions(+), 3 deletions(-) create mode 100644 pyomo/contrib/coramin/algorithms/bnb/bnb.py create mode 100644 pyomo/contrib/coramin/cutting_planes/__init__.py create mode 100644 pyomo/contrib/coramin/cutting_planes/alpha_bb_cuts.py create mode 100644 pyomo/contrib/coramin/cutting_planes/base.py create mode 100644 pyomo/contrib/coramin/heuristics/__init__.py create mode 100644 pyomo/contrib/coramin/heuristics/diving.py create mode 100644 pyomo/contrib/coramin/heuristics/feasibility_pump.py diff --git a/pyomo/contrib/appsi/fbbt.py b/pyomo/contrib/appsi/fbbt.py index 92a0e0c8cbc..2f9424604ac 100644 --- a/pyomo/contrib/appsi/fbbt.py +++ b/pyomo/contrib/appsi/fbbt.py @@ -136,7 +136,8 @@ def _add_params(self, params: List[_ParamData]): cparams = cmodel.create_params(len(params)) for ndx, p in enumerate(params): cp = cparams[ndx] - cp.value = p.value + if p.value is not None: + cp.value = p.value self._param_map[id(p)] = cp if self._symbolic_solver_labels: for ndx, p in enumerate(params): @@ -211,7 +212,8 @@ def _update_variables(self, variables: List[_GeneralVarData]): def update_params(self): for p_id, p in self._params.items(): cp = self._param_map[p_id] - cp.value = p.value + if p.value is not None: + cp.value = p.value def set_objective(self, obj: _GeneralObjectiveData): if self._symbolic_solver_labels: diff --git a/pyomo/contrib/coramin/algorithms/bnb/bnb.py b/pyomo/contrib/coramin/algorithms/bnb/bnb.py new file mode 100644 index 00000000000..ae1f6b7c26f --- /dev/null +++ b/pyomo/contrib/coramin/algorithms/bnb/bnb.py @@ -0,0 +1,116 @@ +import pybnb +from pyomo.core.base.block import _BlockData +from pyomo.common.modeling import unique_component_name +import pyomo.environ as pe +from pyomo.core.expr.visitor import identify_variables +from pyomo.common.collections import ComponentSet, ComponentMap +from pyomo.contrib import appsi +from pyomo.common.config import ConfigDict, ConfigValue, PositiveFloat +from pyomo.contrib.coramin.clone import clone_active_flat +from pyomo.contrib.coramin.relaxations.auto_relax import relax +from pyomo.repn.standard_repn import generate_standard_repn, StandardRepn +from pyomo.contrib.coramin.relaxations.split_expr import split_expr +from pyomo.core.expr.numeric_expr import LinearExpression + + +def _get_clone_and_var_map(m1: _BlockData): + orig_vars = list() + for c in cm.nonrelaxation_component_data_objects( + m1, pe.Constraint, active=True, descend_into=True + ): + for v in identify_variables(c.body, include_fixed=False): + orig_vars.append(v) + obj = cm.get_objective(m1) + if obj is not None: + for v in identify_variables(obj.expr, include_fixed=False): + orig_vars.append(v) + for r in cm.relaxation_data_objects(m1, descend_into=True, active=True): + orig_vars.extend(r.get_rhs_vars()) + orig_vars.append(r.get_aux_var()) + orig_vars = list(ComponentSet(orig_vars)) + tmp_name = unique_component_name(m1, "active_vars") + setattr(m1, tmp_name, orig_vars) + m2 = m1.clone() + new_vars = getattr(m2, tmp_name) + var_map = ComponentMap(zip(new_vars, orig_vars)) + delattr(m1, tmp_name) + delattr(m1, tmp_name) + return m2, var_map + + +class BnBConfig(ConfigDict): + def __init__( + self, + description=None, + doc=None, + implicit=False, + implicit_domain=None, + visibility=0, + ): + super().__init__(description, doc, implicit, implicit_domain, visibility) + self.feasibility_tol = self.declare( + "feasibility_tol", ConfigValue(domain=PositiveFloat, default=1e-8) + ) + + +def impose_structure(m): + m.aux = pe.VarList() + + for key, c in list(m.nonlinear.cons.items()): + repn: StandardRepn = generate_standard_repn(c.body, quadratic=False, compute_values=True) + expr_list = split_expr(repn.nonlinear_expr) + if len(expr_list) == 1: + continue + + linear_coefs = list(repn.linear_coefs) + linear_vars = list(repn.linear_vars) + for term in expr_list: + v = m.aux.add() + linear_coefs.append(1) + linear_vars.append(v) + m.vars.append(v) + m.nonlinear.cons.add(v == term) + new_expr = LinearExpression(constant=repn.constant, linear_coefs=linear_coefs, linear_vars=linear_vars) + m.linear.cons.add((c.lb, new_expr, c.ub)) + del m.nonlinear.cons[key] + + +def _fix_vars_with_close_bounds(m, tol=1e-12): + for v in m.vars: + lb, ub = v.bounds + if abs(ub - lb) <= tol * min(abs(lb), abs(ub)): + v.fix(0.5 * (lb + ub)) + if v.is_fixed(): + v.setlb(v.value) + v.setub(v.value) + + +def find_cut_generators(m): + + + +class _BnB(pybnb.Problem): + def __init__(self, model: _BlockData, config: BnBConfig): + # remove all parameters, fixed variables, etc. + nlp, relaxation = clone_active_flat(model, 2) + self.nlp: _BlockData = nlp + relaxation: _BlockData = relaxation + self.config = config + + # perform fbbt before constructing relaxations in case + # we can identify things like x**3 is convex because + # x >= 0 + self.interval_tightener = it = appsi.fbbt.IntervalTightener() + it.config.deactivate_satisfied_constraints = True + it.config.feasibility_tol = config.feasibility_tol + it.perform_fbbt(self.nlp) + _fix_vars_with_close_bounds(self.nlp) + + impose_structure(relaxation) + find_cut_generators(relaxation) + self.relaxation = relaxation = relax(relaxation) + + +def solve_with_bnb(model: _BlockData, config: BnBConfig): + # we don't want to modify the original model + model, orig_var_map = _get_clone_and_var_map(model) diff --git a/pyomo/contrib/coramin/cutting_planes/__init__.py b/pyomo/contrib/coramin/cutting_planes/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/pyomo/contrib/coramin/cutting_planes/alpha_bb_cuts.py b/pyomo/contrib/coramin/cutting_planes/alpha_bb_cuts.py new file mode 100644 index 00000000000..1587adc8482 --- /dev/null +++ b/pyomo/contrib/coramin/cutting_planes/alpha_bb_cuts.py @@ -0,0 +1,36 @@ +from pyomo.core.base.block import _BlockData +from .base import CutGenerator +from pyomo.core.base.var import _GeneralVarData +from pyomo.core.expr.numeric_expr import NumericExpression +from pyomo.core.expr.visitor import identify_variables +from typing import List, Optional +from pyomo.contrib.coramin.relaxations.hessian import Hessian +from pyomo.contrib.coramin.utils.coramin_enums import EigenValueBounder +from pyomo.contrib.appsi.base import Solver +from pyomo.core.expr.visitor import value +from pyomo.core.expr.relational_expr import RelationalExpression +from pyomo.core.expr.taylor_series import taylor_series_expansion + + +class AlphaBBCutGenerator(CutGenerator): + def __init__(self, lhs: _GeneralVarData, rhs: NumericExpression, eigenvalue_opt: Optional[Solver] = None, method: EigenValueBounder = EigenValueBounder.GershgorinWithSimplification) -> None: + self.lhs = lhs + self.rhs = rhs + self.xlist: List[_GeneralVarData] = list(identify_variables(rhs, include_fixed=False)) + self.hessian = Hessian(expr=rhs, opt=eigenvalue_opt, method=method) + + def generate(self, model: _BlockData, solver: Solver | None = None) -> Optional[RelationalExpression]: + lhs_val = value(self.lhs) + if lhs_val >= value(self.rhs): + return None + + alpha = max(0, -0.5 * self.hessian.get_maximum_eigenvalue()) + alpha_sum = 0 + for ndx, v in enumerate(self.xlist): + lb, ub = v.bounds + alpha_sum += (v - lb) * (v - ub) + alpha_bb_rhs = self.rhs + alpha * alpha_sum + if lhs_val >= value(alpha_bb_rhs): + return None + + return self.lhs >= taylor_series_expansion(alpha_bb_rhs) diff --git a/pyomo/contrib/coramin/cutting_planes/base.py b/pyomo/contrib/coramin/cutting_planes/base.py new file mode 100644 index 00000000000..3cd5edc0501 --- /dev/null +++ b/pyomo/contrib/coramin/cutting_planes/base.py @@ -0,0 +1,10 @@ +from abc import ABC, abstractmethod +from pyomo.core.base.block import _BlockData +from pyomo.contrib import appsi +from typing import Optional + + +class CutGenerator(ABC): + @abstractmethod + def generate(self, model: _BlockData, solver: Optional[appsi.base.Solver] = None): + pass diff --git a/pyomo/contrib/coramin/heuristics/__init__.py b/pyomo/contrib/coramin/heuristics/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/pyomo/contrib/coramin/heuristics/diving.py b/pyomo/contrib/coramin/heuristics/diving.py new file mode 100644 index 00000000000..87df3fd6a95 --- /dev/null +++ b/pyomo/contrib/coramin/heuristics/diving.py @@ -0,0 +1,164 @@ +import pyomo.environ as pe +import pybnb +from pyomo.core.base.block import _BlockData +from pyomo.core.base.var import _GeneralVarData +from typing import Tuple, List, Sequence +from pyomo.contrib import appsi +from pyomo.contrib.coramin.utils.pyomo_utils import get_objective +import numpy as np +import math +from pyomo.common.collections import ComponentSet +from pyomo.core.expr.visitor import identify_variables + +np.set_printoptions(linewidth=1000) + + +def collect_integer_vars(m: _BlockData) -> Tuple[List[_GeneralVarData], List[_GeneralVarData]]: + binary_vars = ComponentSet() + integer_vars = ComponentSet() + for c in m.component_data_objects(pe.Constraint, active=True, descend_into=pe.Block): + for v in identify_variables(c.body, include_fixed=False): + if v.is_binary(): + binary_vars.add(v) + elif v.is_integer(): + integer_vars.add(v) + obj = get_objective(m) + if obj is not None: + for v in identify_variables(obj.expr, include_fixed=False): + if v.is_binary(): + binary_vars.add(v) + elif v.is_integer(): + integer_vars.add(v) + return list(binary_vars), list(integer_vars) + + +def relax_integers(binary_vars: Sequence[_GeneralVarData], integer_vars: Sequence[_GeneralVarData]): + for v in list(binary_vars) + list(integer_vars): + lb, ub = v.bounds + v.domain = pe.Reals + v.setlb(lb) + v.setub(ub) + + +def restore_integers(binary_vars: Sequence[_GeneralVarData], integer_vars: Sequence[_GeneralVarData]): + for v in binary_vars: + v.domain = pe.Binary + for v in integer_vars: + v.domain = pe.Integers + + +def check_feasible(binary_vars: Sequence[_GeneralVarData], integer_vars: Sequence[_GeneralVarData], integer_tol=1e-4): + feas = True + for v in list(binary_vars) + list(integer_vars): + v_val = v.value + if not math.isclose(v_val, round(v_val), abs_tol=integer_tol, rel_tol=0): + feas = False + break + return feas + + +class DivingHeuristic(pybnb.Problem): + def __init__(self, m: _BlockData, nlp_solver: appsi.base.Solver) -> None: + super().__init__() + + nlp_solver.config.load_solution = False + + binary_vars, integer_vars = collect_integer_vars(m) + relax_integers(binary_vars, integer_vars) + + self.m = m + self.opt = nlp_solver + self.binary_vars = binary_vars + self.integer_vars = integer_vars + self.bin_and_int_vars = list(binary_vars) + list(integer_vars) + self.obj = get_objective(m) + + if self.obj.sense == pe.minimize: + self._sense = pybnb.minimize + else: + self._sense = pybnb.maximize + + self.current_node = None + + def sense(self): + return self._sense + + def bound(self): + res = self.opt.solve(self.m) + if res.best_feasible_objective is None: + return self.infeasible_objective() + res.solution_loader.load_vars([v for v in self.bin_and_int_vars if not v.is_fixed()]) + ret = res.best_feasible_objective + if self._sense == pybnb.minimize: + ret = max(self.current_node.bound, ret) + else: + ret = min(self.current_node.bound, ret) + return ret + + def objective(self): + unfixed_vars = [v for v in self.bin_and_int_vars if not v.is_fixed()] + vals = [v.value for v in unfixed_vars] + for v in unfixed_vars: + v.fix(round(v.value)) + res = self.opt.solve(self.m) + if res.best_feasible_objective is None: + ret = self.infeasible_objective() + else: + ret = res.best_feasible_objective + for v, val in zip(unfixed_vars, vals): + v.unfix() + v.set_value(val, skip_validation=True) + return ret + + def get_state(self): + xl = [math.ceil(v.lb) for v in self.bin_and_int_vars] + xl = np.array(xl, dtype=int) + + xu = [math.floor(v.ub) for v in self.bin_and_int_vars] + xu = np.array(xu, dtype=int) + + return xl, xu + + def save_state(self, node): + node.state = self.get_state() + + def load_state(self, node): + self.current_node = node + xl, xu = node.state + xl = [int(i) for i in xl] + xu = [int(i) for i in xu] + + for v, lb, ub in zip(self.bin_and_int_vars, xl, xu): + v.setlb(lb) + v.setub(ub) + if lb == ub: + v.fix(lb) + else: + v.unfix() + + def branch(self): + xl, xu = self.get_state() + dist_list = [(abs(v.value - round(v.value)), ndx) for ndx, v in enumerate(self.bin_and_int_vars)] + dist_list.sort(key=lambda i: i[0], reverse=True) + ndx = dist_list[0][1] + branching_var = self.bin_and_int_vars[ndx] + + xl1 = xl.copy() + xu1 = xu.copy() + xu1[ndx] = math.floor(branching_var.value) + child1 = pybnb.Node() + child1.state = (xl1, xu1) + + xl2 = xl.copy() + xu2 = xu.copy() + xl2[ndx] = math.ceil(branching_var.value) + child2 = pybnb.Node() + child2.state = (xl2, xu2) + + yield child1 + yield child2 + + +def run_diving_heuristic(m: _BlockData, nlp_solver: appsi.base.Solver): + prob = DivingHeuristic(m, nlp_solver) + res = pybnb.solve(prob, queue_strategy=pybnb.QueueStrategy.bound, objective_stop=prob.infeasible_objective()) diff --git a/pyomo/contrib/coramin/heuristics/feasibility_pump.py b/pyomo/contrib/coramin/heuristics/feasibility_pump.py new file mode 100644 index 00000000000..efd8ee1ba91 --- /dev/null +++ b/pyomo/contrib/coramin/heuristics/feasibility_pump.py @@ -0,0 +1,198 @@ +import pyomo.environ as pe +from pyomo.core.base.block import _BlockData +from pyomo.contrib.appsi.base import Solver +from pyomo.common.collections import ComponentSet +from pyomo.core.expr.visitor import identify_variables +from pyomo.contrib.coramin.utils.pyomo_utils import get_objective +from typing import List, Sequence, Tuple +from pyomo.core.base.var import _GeneralVarData +import math +import time +from pyomo.common.modeling import unique_component_name +import random +import numpy as np + + +def collect_integer_vars(m: _BlockData) -> Tuple[List[_GeneralVarData], List[_GeneralVarData]]: + binary_vars = ComponentSet() + integer_vars = ComponentSet() + for c in m.component_data_objects(pe.Constraint, active=True, descend_into=pe.Block): + for v in identify_variables(c.body, include_fixed=False): + if v.is_binary(): + binary_vars.add(v) + elif v.is_integer(): + integer_vars.add(v) + obj = get_objective(m) + if obj is not None: + for v in identify_variables(obj.expr, include_fixed=False): + if v.is_binary(): + binary_vars.add(v) + elif v.is_integer(): + integer_vars.add(v) + return list(binary_vars), list(integer_vars) + + +def relax_integers(binary_vars: Sequence[_GeneralVarData], integer_vars: Sequence[_GeneralVarData]): + for v in list(binary_vars) + list(integer_vars): + lb, ub = v.bounds + v.domain = pe.Reals + v.setlb(lb) + v.setub(ub) + + +def restore_integers(binary_vars: Sequence[_GeneralVarData], integer_vars: Sequence[_GeneralVarData]): + for v in binary_vars: + v.domain = pe.Binary + for v in integer_vars: + v.domain = pe.Integers + + +def check_feasible(binary_vars: Sequence[_GeneralVarData], integer_vars: Sequence[_GeneralVarData], integer_tol=1e-4): + feas = True + for v in list(binary_vars) + list(integer_vars): + v_val = v.value + if not math.isclose(v_val, round(v_val), abs_tol=integer_tol, rel_tol=0): + print(v_val) + feas = False + break + return feas + +def run_feasibility_pump(m: _BlockData, nlp_solver: Solver, time_limit: float = math.inf, iter_limit=300, integer_tol=1e-4, use_fixing: bool = False, use_flip: bool = True): + t0 = time.time() + + binary_vars, integer_vars = collect_integer_vars(m) + relax_integers(binary_vars, integer_vars) + + nlp_solver.config.load_solution = False + + res = nlp_solver.solve(m) + if res.best_feasible_objective is None: + restore_integers(binary_vars, integer_vars) + return None + + res.solution_loader.load_vars(binary_vars) + res.solution_loader.load_vars(integer_vars) + is_feas = check_feasible(binary_vars, integer_vars, integer_tol) + if is_feas: + restore_integers(binary_vars, integer_vars) + res.load_vars() + return res + + orig_obj = get_objective(m) + orig_obj.deactivate() + + feasible_results = None + new_obj_name = unique_component_name(m, 'fp_obj') + last_target_binary_vals = None + last_target_integer_vals = None + n_bin = len(binary_vars) + for _iter in range(iter_limit): + if time.time() - t0 > time_limit: + break + + if hasattr(m, new_obj_name): + delattr(m, new_obj_name) + + target_binary_vals = [round(v.value) for v in binary_vars] + target_integer_vals = [round(v.value) for v in integer_vars] + + dist_list = list() + ndx = 0 + for v, val in zip(binary_vars, target_binary_vals): + dist_list.append((ndx, abs(v.value - val))) + ndx += 1 + for v, val in zip(integer_vars, target_integer_vals): + dist_list.append((ndx, abs(v.value - val))) + ndx += 1 + dist_list.sort(key=lambda i: i[1], reverse=True) + + if use_fixing: + ndx_to_fix = None + ndx = len(binary_vars) + len(integer_vars) - 1 + while ndx >= 0: + tmp = dist_list[ndx][0] + if tmp < n_bin: + if not binary_vars[tmp].is_fixed(): + ndx_to_fix = tmp + break + else: + if not integer_vars[tmp - n_bin].is_fixed(): + ndx_to_fix = tmp + break + ndx -= 1 + if ndx_to_fix < n_bin: + binary_vars[ndx_to_fix].fix(target_binary_vals[ndx_to_fix]) + else: + _ndx = ndx_to_fix - n_bin + integer_vars[_ndx].fix(target_integer_vals[_ndx]) + + if last_target_binary_vals is not None and use_flip: + if target_binary_vals == last_target_binary_vals and target_integer_vals == last_target_integer_vals: + print('flipping') + T = math.floor(0.5*(len(binary_vars) + len(integer_vars))) + T = 10 + num_flip = random.randint(math.floor(0.5*T), math.ceil(1.5*T)) + dist_list = list() + ndx = 0 + for v, val in zip(binary_vars, target_binary_vals): + dist_list.append((ndx, abs(v.value - val))) + ndx += 1 + for v, val in zip(integer_vars, target_integer_vals): + dist_list.append((ndx, abs(v.value - val))) + ndx += 1 + dist_list.sort(key=lambda i: i[1], reverse=True) + indices_to_flip = [i[0] for i in dist_list[:num_flip]] + for ndx in indices_to_flip: + if ndx < n_bin: + if target_binary_vals[ndx] == 0: + target_binary_vals[ndx] = 1 + else: + assert target_binary_vals[ndx] == 1 + target_binary_vals[ndx] = 0 + else: + _ndx = ndx - n_bin + if target_integer_vals[_ndx] == 0: + target_integer_vals[_ndx] = 1 + else: + assert target_integer_vals[_ndx] == 1 + target_integer_vals[_ndx] = 0 + + last_target_binary_vals = target_binary_vals + last_target_integer_vals = target_integer_vals + + obj_expr = 0 + for v, val in zip(binary_vars, target_binary_vals): + if val == 0: + obj_expr += v + else: + assert val == 1 + obj_expr += (1 - v) + for v, val in zip(integer_vars, target_integer_vals): + obj_expr += (v - val)**2 + setattr(m, new_obj_name, pe.Objective(expr=obj_expr)) + + res = nlp_solver.solve(m) + if res.best_feasible_objective is None: + print('failed') + break + res.solution_loader.load_vars([v for v in binary_vars if not v.is_fixed()]) + res.solution_loader.load_vars([v for v in integer_vars if not v.is_fixed()]) + + is_feas = check_feasible(binary_vars, integer_vars, integer_tol) + if is_feas: + feasible_results = res + break + + restore_integers(binary_vars, integer_vars) + orig_obj.activate() + if hasattr(m, new_obj_name): + delattr(m, new_obj_name) + if feasible_results is not None: + feasible_results.solution_loader.load_vars() + for v in binary_vars: + v.unfix() + for v in integer_vars: + v.unfix() + print(orig_obj.expr()) + + return feasible_results diff --git a/pyomo/contrib/fbbt/interval.py b/pyomo/contrib/fbbt/interval.py index fd86af4c106..83f134c9974 100644 --- a/pyomo/contrib/fbbt/interval.py +++ b/pyomo/contrib/fbbt/interval.py @@ -81,7 +81,16 @@ def inv(xl, xu, feasibility_tol): def div(xl, xu, yl, yu, feasibility_tol): - return mul(xl, xu, *inv(yl, yu, feasibility_tol)) + lb, ub = mul(xl, xu, *inv(yl, yu, feasibility_tol)) + if xl >= 0 and yl >= 0: + lb = max(lb, 0) + elif xu <= 0 and yu <= 0: + lb = max(lb, 0) + elif xl >= 0 and yu <= 0: + ub = min(ub, 0) + elif xu <= 0 and yl >= 0: + ub = min(ub, 0) + return lb, ub def power(xl, xu, yl, yu, feasibility_tol): From 52b6ad3505f07df0546e41cd03a83b9cabf5ccae Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Fri, 22 Dec 2023 21:02:45 -0700 Subject: [PATCH 021/128] clean up diving heuristic --- pyomo/contrib/coramin/heuristics/diving.py | 89 ++++++++++++++++------ 1 file changed, 66 insertions(+), 23 deletions(-) diff --git a/pyomo/contrib/coramin/heuristics/diving.py b/pyomo/contrib/coramin/heuristics/diving.py index 87df3fd6a95..0c9111868d6 100644 --- a/pyomo/contrib/coramin/heuristics/diving.py +++ b/pyomo/contrib/coramin/heuristics/diving.py @@ -2,22 +2,26 @@ import pybnb from pyomo.core.base.block import _BlockData from pyomo.core.base.var import _GeneralVarData -from typing import Tuple, List, Sequence +from typing import Tuple, List, Sequence, Optional, MutableMapping from pyomo.contrib import appsi from pyomo.contrib.coramin.utils.pyomo_utils import get_objective import numpy as np import math -from pyomo.common.collections import ComponentSet +from pyomo.common.collections import ComponentSet, ComponentMap from pyomo.core.expr.visitor import identify_variables +from mpi4py import MPI +import time np.set_printoptions(linewidth=1000) -def collect_integer_vars(m: _BlockData) -> Tuple[List[_GeneralVarData], List[_GeneralVarData]]: +def collect_vars(m: _BlockData) -> Tuple[List[_GeneralVarData], List[_GeneralVarData], List[_GeneralVarData]]: binary_vars = ComponentSet() integer_vars = ComponentSet() + all_vars = ComponentSet() for c in m.component_data_objects(pe.Constraint, active=True, descend_into=pe.Block): for v in identify_variables(c.body, include_fixed=False): + all_vars.add(v) if v.is_binary(): binary_vars.add(v) elif v.is_integer(): @@ -25,11 +29,12 @@ def collect_integer_vars(m: _BlockData) -> Tuple[List[_GeneralVarData], List[_Ge obj = get_objective(m) if obj is not None: for v in identify_variables(obj.expr, include_fixed=False): + all_vars.add(v) if v.is_binary(): binary_vars.add(v) elif v.is_integer(): integer_vars.add(v) - return list(binary_vars), list(integer_vars) + return list(binary_vars), list(integer_vars), list(all_vars) def relax_integers(binary_vars: Sequence[_GeneralVarData], integer_vars: Sequence[_GeneralVarData]): @@ -47,30 +52,23 @@ def restore_integers(binary_vars: Sequence[_GeneralVarData], integer_vars: Seque v.domain = pe.Integers -def check_feasible(binary_vars: Sequence[_GeneralVarData], integer_vars: Sequence[_GeneralVarData], integer_tol=1e-4): - feas = True - for v in list(binary_vars) + list(integer_vars): - v_val = v.value - if not math.isclose(v_val, round(v_val), abs_tol=integer_tol, rel_tol=0): - feas = False - break - return feas - - class DivingHeuristic(pybnb.Problem): def __init__(self, m: _BlockData, nlp_solver: appsi.base.Solver) -> None: super().__init__() nlp_solver.config.load_solution = False - binary_vars, integer_vars = collect_integer_vars(m) + binary_vars, integer_vars, all_vars = collect_vars(m) relax_integers(binary_vars, integer_vars) self.m = m self.opt = nlp_solver + self.all_vars = all_vars self.binary_vars = binary_vars self.integer_vars = integer_vars self.bin_and_int_vars = list(binary_vars) + list(integer_vars) + self.orig_lbs = [v.lb for v in self.bin_and_int_vars] + self.orig_ubs = [v.ub for v in self.bin_and_int_vars] self.obj = get_objective(m) if self.obj.sense == pe.minimize: @@ -78,7 +76,7 @@ def __init__(self, m: _BlockData, nlp_solver: appsi.base.Solver) -> None: else: self._sense = pybnb.maximize - self.current_node = None + self.current_node: Optional[pybnb.Node] = None def sense(self): return self._sense @@ -105,8 +103,13 @@ def objective(self): ret = self.infeasible_objective() else: ret = res.best_feasible_objective + res.solution_loader.load_vars() + sol = np.array([v.value for v in self.all_vars], dtype=float) + xl, xu, _ = self.current_node.state + self.current_node.state = (xl, xu, sol) for v, val in zip(unfixed_vars, vals): v.unfix() + # we have to restore the values so that branch() works properly v.set_value(val, skip_validation=True) return ret @@ -117,14 +120,14 @@ def get_state(self): xu = [math.floor(v.ub) for v in self.bin_and_int_vars] xu = np.array(xu, dtype=int) - return xl, xu + return xl, xu, None def save_state(self, node): node.state = self.get_state() def load_state(self, node): self.current_node = node - xl, xu = node.state + xl, xu, _ = node.state xl = [int(i) for i in xl] xu = [int(i) for i in xu] @@ -137,7 +140,7 @@ def load_state(self, node): v.unfix() def branch(self): - xl, xu = self.get_state() + xl, xu, _ = self.get_state() dist_list = [(abs(v.value - round(v.value)), ndx) for ndx, v in enumerate(self.bin_and_int_vars)] dist_list.sort(key=lambda i: i[0], reverse=True) ndx = dist_list[0][1] @@ -147,18 +150,58 @@ def branch(self): xu1 = xu.copy() xu1[ndx] = math.floor(branching_var.value) child1 = pybnb.Node() - child1.state = (xl1, xu1) + child1.state = (xl1, xu1, None) xl2 = xl.copy() xu2 = xu.copy() xl2[ndx] = math.ceil(branching_var.value) child2 = pybnb.Node() - child2.state = (xl2, xu2) + child2.state = (xl2, xu2, None) yield child1 yield child2 -def run_diving_heuristic(m: _BlockData, nlp_solver: appsi.base.Solver): +def assert_feasible(m: _BlockData, var_list: Sequence[_GeneralVarData], feasibility_tol: float, integer_tol: float): + for c in m.component_data_objects(pe.Constraint, active=True, descend_into=pe.Block): + body_val = pe.value(c.body) + if c.lb is not None: + assert c.lb - feasibility_tol <= body_val or abs(c.lb - body_val)/abs(c.lb) <= feasibility_tol + if c.ub is not None: + assert body_val <= c.ub + feasibility_tol or abs(c.ub - body_val)/abs(c.ub) <= feasibility_tol + + for v in var_list: + val = v.value + lb, ub = v.bounds + if lb is not None: + assert lb - feasibility_tol <= val or abs(lb - val)/abs(lb) <= feasibility_tol + if ub is not None: + assert val <= ub + feasibility_tol or abs(ub - val)/abs(ub) <= feasibility_tol + if v.is_integer(): + assert abs(val - round(val)) <= integer_tol + + +def run_diving_heuristic(m: _BlockData, nlp_solver: appsi.base.Solver, feasibility_tol: float = 1e-6, integer_tol: float = 1e-4, time_limit: float = 300, node_limit: int = 1000): prob = DivingHeuristic(m, nlp_solver) - res = pybnb.solve(prob, queue_strategy=pybnb.QueueStrategy.bound, objective_stop=prob.infeasible_objective()) + res: pybnb.SolverResults = pybnb.solve(prob, queue_strategy=pybnb.QueueStrategy.bound, objective_stop=prob.infeasible_objective(), node_limit=node_limit, time_limit=time_limit) + ss = pybnb.SolutionStatus + if res.solution_status in {ss.feasible, ss.optimal}: + best_obj = res.objective + best_sol: MutableMapping[_GeneralVarData, float] = ComponentMap(zip(prob.all_vars, res.best_node.state[2])) + else: + best_obj = None + best_sol = None + + restore_integers(prob.binary_vars, prob.integer_vars) + for v, lb, ub in zip(prob.bin_and_int_vars, prob.orig_lbs, prob.orig_ubs): + v.unfix() + v.setlb(lb) + v.setub(ub) + + if best_sol is not None: + # double check that the solution is feasible + for v, val in best_sol.items(): + v.set_value(val, skip_validation=True) + assert_feasible(m, prob.all_vars, feasibility_tol, integer_tol) + + return best_obj, best_sol From cb09908d6a5ff2a0426b4746382db771294fae0e Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 26 Dec 2023 03:34:26 -0700 Subject: [PATCH 022/128] revert changes to interval.py --- pyomo/contrib/fbbt/interval.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/pyomo/contrib/fbbt/interval.py b/pyomo/contrib/fbbt/interval.py index 83f134c9974..e94d283f962 100644 --- a/pyomo/contrib/fbbt/interval.py +++ b/pyomo/contrib/fbbt/interval.py @@ -82,14 +82,6 @@ def inv(xl, xu, feasibility_tol): def div(xl, xu, yl, yu, feasibility_tol): lb, ub = mul(xl, xu, *inv(yl, yu, feasibility_tol)) - if xl >= 0 and yl >= 0: - lb = max(lb, 0) - elif xu <= 0 and yu <= 0: - lb = max(lb, 0) - elif xl >= 0 and yu <= 0: - ub = min(ub, 0) - elif xu <= 0 and yl >= 0: - ub = min(ub, 0) return lb, ub From 205811e696a3b2b317cad0f2efe4340cb89dde5b Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 26 Dec 2023 11:01:16 -0700 Subject: [PATCH 023/128] working on branch and bound --- pyomo/contrib/coramin/algorithms/bnb/bnb.py | 235 +++++++++++++++- pyomo/contrib/coramin/clone.py | 45 ++- .../contrib/coramin/relaxations/auto_relax.py | 265 +++++------------- .../coramin/relaxations/relaxations_base.py | 26 +- 4 files changed, 343 insertions(+), 228 deletions(-) diff --git a/pyomo/contrib/coramin/algorithms/bnb/bnb.py b/pyomo/contrib/coramin/algorithms/bnb/bnb.py index ae1f6b7c26f..f9cd1152e09 100644 --- a/pyomo/contrib/coramin/algorithms/bnb/bnb.py +++ b/pyomo/contrib/coramin/algorithms/bnb/bnb.py @@ -11,20 +11,31 @@ from pyomo.repn.standard_repn import generate_standard_repn, StandardRepn from pyomo.contrib.coramin.relaxations.split_expr import split_expr from pyomo.core.expr.numeric_expr import LinearExpression +from pyomo.contrib.coramin.relaxations import iterators +from pyomo.contrib.coramin.utils.pyomo_utils import get_objective +from pyomo.core.base.block import _BlockData +from pyomo.core.base.var import _GeneralVarData +from typing import Tuple, List, Sequence +import math +import numpy as np +import logging + + +logger = logging.getLogger(__name__) def _get_clone_and_var_map(m1: _BlockData): orig_vars = list() - for c in cm.nonrelaxation_component_data_objects( + for c in iterators.nonrelaxation_component_data_objects( m1, pe.Constraint, active=True, descend_into=True ): for v in identify_variables(c.body, include_fixed=False): orig_vars.append(v) - obj = cm.get_objective(m1) + obj = get_objective(m1) if obj is not None: for v in identify_variables(obj.expr, include_fixed=False): orig_vars.append(v) - for r in cm.relaxation_data_objects(m1, descend_into=True, active=True): + for r in iterators.relaxation_data_objects(m1, descend_into=True, active=True): orig_vars.extend(r.get_rhs_vars()) orig_vars.append(r.get_aux_var()) orig_vars = list(ComponentSet(orig_vars)) @@ -34,7 +45,7 @@ def _get_clone_and_var_map(m1: _BlockData): new_vars = getattr(m2, tmp_name) var_map = ComponentMap(zip(new_vars, orig_vars)) delattr(m1, tmp_name) - delattr(m1, tmp_name) + delattr(m2, tmp_name) return m2, var_map @@ -51,6 +62,38 @@ def __init__( self.feasibility_tol = self.declare( "feasibility_tol", ConfigValue(domain=PositiveFloat, default=1e-8) ) + self.lp_solver = self.declare("lp_solver", ConfigValue()) + self.nlp_solver = self.declare("nlp_solver", ConfigValue()) + self.abs_gap = self.declare("abs_gap", ConfigValue(default=1e-4)) + self.rel_gap = self.declare("rel_gap", ConfigValue(default=1e-3)) + self.integer_tol = self.declare("integer_tol", ConfigValue(default=1e-4)) + + +def collect_vars(m: _BlockData) -> Tuple[List[_GeneralVarData], List[_GeneralVarData], List[_GeneralVarData]]: + binary_vars = ComponentSet() + integer_vars = ComponentSet() + for c in m.component_data_objects(pe.Constraint, active=True, descend_into=pe.Block): + for v in identify_variables(c.body, include_fixed=False): + if v.is_binary(): + binary_vars.add(v) + elif v.is_integer(): + integer_vars.add(v) + obj = get_objective(m) + if obj is not None: + for v in identify_variables(obj.expr, include_fixed=False): + if v.is_binary(): + binary_vars.add(v) + elif v.is_integer(): + integer_vars.add(v) + return list(binary_vars), list(integer_vars) + + +def relax_integers(binary_vars: Sequence[_GeneralVarData], integer_vars: Sequence[_GeneralVarData]): + for v in list(binary_vars) + list(integer_vars): + lb, ub = v.bounds + v.domain = pe.Reals + v.setlb(lb) + v.setub(ub) def impose_structure(m): @@ -77,16 +120,14 @@ def impose_structure(m): def _fix_vars_with_close_bounds(m, tol=1e-12): for v in m.vars: - lb, ub = v.bounds - if abs(ub - lb) <= tol * min(abs(lb), abs(ub)): - v.fix(0.5 * (lb + ub)) if v.is_fixed(): v.setlb(v.value) v.setub(v.value) - - -def find_cut_generators(m): - + lb, ub = v.bounds + if lb is None or ub is None: + continue + if abs(ub - lb) <= tol * min(abs(lb), abs(ub)): + v.fix(0.5 * (lb + ub)) class _BnB(pybnb.Problem): @@ -96,6 +137,9 @@ def __init__(self, model: _BlockData, config: BnBConfig): self.nlp: _BlockData = nlp relaxation: _BlockData = relaxation self.config = config + self.config.lp_solver.config.load_solution = False + self.config.lp_solver.update_config.treat_fixed_vars_as_params = True + self.config.nlp_solver.config.load_solution = False # perform fbbt before constructing relaxations in case # we can identify things like x**3 is convex because @@ -103,14 +147,175 @@ def __init__(self, model: _BlockData, config: BnBConfig): self.interval_tightener = it = appsi.fbbt.IntervalTightener() it.config.deactivate_satisfied_constraints = True it.config.feasibility_tol = config.feasibility_tol - it.perform_fbbt(self.nlp) - _fix_vars_with_close_bounds(self.nlp) + it.perform_fbbt(relaxation) + _fix_vars_with_close_bounds(relaxation) impose_structure(relaxation) - find_cut_generators(relaxation) + #find_cut_generators(relaxation) self.relaxation = relaxation = relax(relaxation) + self.relaxation_objects = list() + for r in iterators.relaxation_data_objects(relaxation, descend_into=True, active=True): + self.relaxation_objects.append(r) + + binary_vars, integer_vars = collect_vars(nlp) + relax_integers(binary_vars, integer_vars) + self.binary_vars = binary_vars + self.integer_vars = integer_vars + self.bin_and_int_vars = list(binary_vars) + list(integer_vars) + int_var_set = ComponentSet(self.bin_and_int_vars) + + self.rhs_vars = list() + for r in self.relaxation_objects: + self.rhs_vars.extend(i for i in r.get_rhs_vars() if not i.is_fixed()) + self.rhs_vars = list(ComponentSet(self.rhs_vars) - int_var_set) + + self.all_branching_vars = list(binary_vars) + list(integer_vars) + list(self.rhs_vars) + self.var_to_ndx_map = ComponentMap((v, ndx) for ndx, v in enumerate(self.all_branching_vars)) + + if get_objective(nlp).sense == pe.minimize: + self._sense = pybnb.minimize + else: + self._sense = pybnb.maximize + + self.current_node: Optional[pybnb.Node] = None + + def sense(self): + return self._sense + + def bound(self): + res = self.config.lp_solver.solve(self.relaxation) + if res.termination_condition == appsi.base.TerminationCondition.infeasible: + return self.infeasible_objective() + if res.termination_condition != appsi.base.TerminationCondition.optimal: + raise RuntimeError(f"Cannot handle termination condition {res.termination_condition} when solving relaxation") + res.solution_loader.load_vars() + return res.best_objective_bound + + def objective(self): + if self.current_node.tree_depth % 10 != 0: + return self.infeasible_objective() + unfixed_vars = [v for v in self.bin_and_int_vars if not v.is_fixed()] + vals = [v.value for v in unfixed_vars] + for v in unfixed_vars: + v.fix(round(v.value)) + res = self.config.nlp_solver.solve(self.nlp) + if res.best_feasible_objective is None: + ret = self.infeasible_objective() + else: + ret = res.best_feasible_objective + for v, val in zip(unfixed_vars, vals): + v.unfix() + # we have to restore the values so that branch() works properly + v.set_value(val, skip_validation=True) + return ret + + def get_state(self): + xl = list() + xu = list() + + for v in self.bin_and_int_vars: + xl.append(math.ceil(v.lb)) + xu.append(math.floor(v.ub)) + + for v in self.rhs_vars: + lb, ub = v.bounds + if lb is None: + xl.append(-math.inf) + else: + xl.append(v.lb) + if xu is None: + xu.append(math.inf) + else: + xu.append(v.ub) + + xl = np.array(xl, dtype=float) + xu = np.array(xu, dtype=float) + + return xl, xu + + def save_state(self, node): + node.state = self.get_state() + + def load_state(self, node): + self.current_node = node + xl, xu = node.state + + xl = [float(i) for i in xl] + xu = [float(i) for i in xu] + + all_vars = self.all_branching_vars + + for v, lb, ub in zip(all_vars, xl, xu): + if math.isfinite(lb): + v.setlb(lb) + else: + v.setlb(None) + if math.isfinite(ub): + v.setub(ub) + else: + v.setub(None) + + if lb == ub: + v.fix(lb) + else: + v.unfix() + + for r in self.relaxation_objects: + r.rebuild() + + def branch(self): + xl, xu = self.get_state() + + var_to_branch_on = None + max_viol = 0 + for v in self.bin_and_int_vars: + err = abs(v.value - round(v.value)) + if err > max_viol and err > self.config.integer_tol: + var_to_branch_on = v + max_viol = err + + if var_to_branch_on is None: + for r in self.relaxation_objects: + err = r.get_deviation() + if err > max_viol and err > self.config.feasibility_tol: + var_to_branch_on = r.get_rhs_vars()[0] + max_viol = err + + if var_to_branch_on is None: + raise NotImplementedError("relaxation was feasible - add handling for this") + + xl1 = xl.copy() + xu1 = xu.copy() + xl2 = xl.copy() + xu2 = xu.copy() + child1 = pybnb.Node() + child2 = pybnb.Node() + + ndx_to_branch_on = self.var_to_ndx_map[var_to_branch_on] + new_lb = new_ub = var_to_branch_on.value + if ndx_to_branch_on < len(self.bin_and_int_vars): + new_ub = math.floor(new_ub) + new_lb = math.ceil(new_lb) + xu1[ndx_to_branch_on] = new_ub + xl2[ndx_to_branch_on] = new_lb + + child1.state = (xl1, xu1) + child2.state = (xl2, xu2) + + yield child1 + yield child2 -def solve_with_bnb(model: _BlockData, config: BnBConfig): +def solve_with_bnb(model: _BlockData, config: BnBConfig, comm=None): # we don't want to modify the original model model, orig_var_map = _get_clone_and_var_map(model) + prob = _BnB(model, config) + res = pybnb.solve( + prob, + queue_strategy=pybnb.QueueStrategy.bound, + absolute_gap=config.abs_gap, + relative_gap=config.rel_gap, + comparison_tolerance=1e-5, + comm=comm, + # log=logger, + ) diff --git a/pyomo/contrib/coramin/clone.py b/pyomo/contrib/coramin/clone.py index b624abd7643..c9a4375631f 100644 --- a/pyomo/contrib/coramin/clone.py +++ b/pyomo/contrib/coramin/clone.py @@ -4,32 +4,47 @@ from .utils.pyomo_utils import get_objective from pyomo.repn.standard_repn import generate_standard_repn from pyomo.common.collections import ComponentSet, ComponentMap +from pyomo.core.base.block import _BlockData +from typing import List -def clone_active_flat(m1): - m2 = pe.Block(concrete=True) - m2.cons = pe.ConstraintList() +def clone_active_flat(m1: _BlockData, num_clones: int = 1) -> List[_BlockData]: + clone_list = [pe.Block(concrete=True) for i in range(num_clones)] + for m2 in clone_list: + m2.linear = pe.Block() + m2.nonlinear = pe.Block() + m2.linear.cons = pe.ConstraintList() + m2.nonlinear.cons = pe.ConstraintList() all_vars = ComponentSet() # constraints for c in iterators.nonrelaxation_component_data_objects( m1, pe.Constraint, active=True, descend_into=True ): - lb = pe.value(c.lower) - ub = pe.value(c.upper) repn = generate_standard_repn(c.body, quadratic=False, compute_values=True) all_vars.update(repn.linear_vars) all_vars.update(repn.nonlinear_vars) body = repn.to_expression() - m2.cons.add((lb, body, ub)) + if repn.nonlinear_expr is None: + for m2 in clone_list: + m2.linear.cons.add((c.lb, body, c.ub)) + else: + for m2 in clone_list: + m2.nonlinear.cons.add((c.lb, body, c.ub)) # objective obj = get_objective(m1) - repn = generate_standard_repn(obj.expr, quadratic=False, compute_values=True) - all_vars.update(repn.linear_vars) - all_vars.update(repn.nonlinear_vars) - obj_expr = repn.to_expression() - m2.obj = pe.Objective(expr=obj_expr, sense=obj.sense) + if obj is not None: + repn = generate_standard_repn(obj.expr, quadratic=False, compute_values=True) + all_vars.update(repn.linear_vars) + all_vars.update(repn.nonlinear_vars) + obj_expr = repn.to_expression() + if repn.nonlinear_expr is None: + for m2 in clone_list: + m2.linear.obj = pe.Objective(expr=obj_expr, sense=obj.sense) + else: + for m2 in clone_list: + m2.nonlinear.obj = pe.Objective(expr=obj_expr, sense=obj.sense) rel_list = list() for r in iterators.relaxation_data_objects(m1, descend_into=True, active=True): @@ -46,8 +61,10 @@ def clone_active_flat(m1): if not aux_var.is_fixed(): all_vars.add(aux_var) new_rel = copy_relaxation_with_local_data(r, var_map) - setattr(m2, f'rel{ndx}', new_rel) + for m2 in clone_list: + setattr(m2, f'rel{ndx}', new_rel) - m2.vars = pe.Reference(list(all_vars)) + for m2 in clone_list: + m2.vars = list(all_vars) - return m2 + return clone_list diff --git a/pyomo/contrib/coramin/relaxations/auto_relax.py b/pyomo/contrib/coramin/relaxations/auto_relax.py index e3de804a8cc..9bea8293a91 100644 --- a/pyomo/contrib/coramin/relaxations/auto_relax.py +++ b/pyomo/contrib/coramin/relaxations/auto_relax.py @@ -48,6 +48,7 @@ from typing import MutableMapping, Tuple, Union, Optional from pyomo.core.base.block import _BlockData from .iterators import relaxation_data_objects +from pyomo.contrib.coramin.utils.pyomo_utils import get_objective logger = logging.getLogger(__name__) @@ -1461,16 +1462,7 @@ def _relax_expr_with_convexity_check( def relax( model, - descend_into=None, - in_place=False, - use_fbbt=True, - fbbt_options=None, - perform_expression_simplification: bool = True, - use_alpha_bb: bool = False, - eigenvalue_bounder: EigenValueBounder = EigenValueBounder.GershgorinWithSimplification, - max_vars_per_alpha_bb: int = 4, - max_eigenvalue_for_alpha_bb: float = 100, - eigenvalue_opt: Optional[appsi.base.Solver] = None, + descend_into=True, ): """ Create a convex relaxation of the model. @@ -1482,95 +1474,42 @@ def relax( descend_into: type or tuple of type, optional The types of pyomo components that should be checked for constraints to be relaxed. The default is (Block, Disjunct). - in_place: bool, optional - If False (default=False), model will be cloned, and the clone will be relaxed. - If True, then model will be modified in place. - use_fbbt: bool, optional - If True (default=True), then FBBT will be used to tighten variable bounds. If False, - FBBT will not be used. - fbbt_options: dict, optional - The options to pass to the call to fbbt. See pyomo.contrib.fbbt.fbbt.fbbt for details. - convexity_effort: ConvexityEffort Returns ------- m: pyomo.core.base.block._BlockData or pyomo.core.base.PyomoModel.ConcreteModel The relaxed model """ - """ - For now, we will use FBBT both before relaxing the model and after relaxing the model. The reason we need to - do it before relaxing the model is that the variable bounds will affect the structure of the relaxation. For - example, if we need to relax x**3 and x >= 0, then we know x**3 is convex, and we can relax it as a - convex, univariate function. However, if x can be positive or negative, then x**3 is neither convex nor concave. - In this case, we relax it by reformulating it as x * x**2. The hope is that performing FBBT before relaxing - the model will help identify things like x >= 0 and therefore x**3 is convex. The correct way to do this is to - update the relaxation classes so that the original expression is known, and the best relaxation can be used - anytime the variable bounds are updated. For example, suppose the model is relaxed and, only after OBBT is - performed, we find out x >= 0. We should be able to easily update the relaxation so that x**3 is then relaxed - as a convex univariate function. The reason FBBT needs to be performed after relaxing the model is that - we want to make sure that all of the auxiliary variables introduced get tightened bounds. The correct way to - handle this is to perform FBBT with the original model with suspect, which forms a DAG. Each auxiliary variable - introduced in the relaxed model corresponds to a node in the DAG. If we use suspect, then we can easily - update the bounds of the auxiliary variables without performing FBBT a second time. - """ - if not in_place: - m = model.clone() - else: - m = model - - if fbbt_options is None: - fbbt_options = dict() - - if use_fbbt: - it = appsi.fbbt.IntervalTightener() - for k, v in fbbt_options.items(): - setattr(it.config, k, v) - original_active_vars = ComponentSet(active_vars(m, include_fixed=False)) - it.perform_fbbt(m) - new_active_vars = ComponentSet(active_vars(m, include_fixed=False)) - # some variables may have become stale by deactivating satisfied constraints, - # so we need to fix them. - for v in original_active_vars - new_active_vars: - v.fix(0.5 * (v.lb + v.ub)) - - if descend_into is None: - descend_into = (pe.Block, Disjunct) + m = pe.Block(concrete=True) + m.cons = pe.ConstraintList() + m.aux_vars = pe.VarList() + m.relaxations = pe.Block() + m.aux_cons = pe.ConstraintList() aux_var_map = dict() - counter_dict = dict() degree_map = ComponentMap() + counter = RelaxationCounter() for c in nonrelaxation_component_data_objects( - m, ctype=Constraint, active=True, descend_into=descend_into, sort=True + model, ctype=Constraint, active=True, descend_into=descend_into, ): - body_degree = polynomial_degree(c.body) - if body_degree is not None: - if body_degree <= 1: - continue + repn = generate_standard_repn(c.body, quadratic=False, compute_values=True) + if repn.nonlinear_expr is None: + m.cons.add((c.lb, c.body, c.ub)) + continue - if c.lower is not None and c.upper is not None: + cl, cu = c.lb, c.ub + if cl is not None and cu is not None: relaxation_side = RelaxationSide.BOTH - elif c.lower is not None: + elif cl is not None: relaxation_side = RelaxationSide.OVER - elif c.upper is not None: + elif cu is not None: relaxation_side = RelaxationSide.UNDER else: raise ValueError( 'Encountered a constraint without a lower or an upper bound: ' + str(c) ) - parent_block = c.parent_block() - - if parent_block in counter_dict: - counter = counter_dict[parent_block] - else: - parent_block.relaxations = pe.Block() - parent_block.aux_vars = pe.VarList() - parent_block.aux_cons = pe.ConstraintList() - counter = RelaxationCounter() - counter_dict[parent_block] = counter - - repn = generate_standard_repn(c.body, quadratic=False, compute_values=False) assert len(repn.quadratic_vars) == 0 assert repn.nonlinear_expr is not None if len(repn.linear_vars) > 0: @@ -1585,131 +1524,75 @@ def relax( relaxation_side_map = ComponentMap() relaxation_side_map[repn.nonlinear_expr] = relaxation_side - if not use_alpha_bb: - new_body += _relax_expr( - expr=repn.nonlinear_expr, - aux_var_map=aux_var_map, - parent_block=parent_block, - relaxation_side_map=relaxation_side_map, - counter=counter, - degree_map=degree_map, - ) - else: - new_body += _relax_expr_with_convexity_check( - orig_expr=repn.nonlinear_expr, - aux_var_map=aux_var_map, - parent_block=parent_block, - relaxation_side_map=relaxation_side_map, - counter=counter, - degree_map=degree_map, - perform_expression_simplification=perform_expression_simplification, - eigenvalue_bounder=eigenvalue_bounder, - max_vars_per_alpha_bb=max_vars_per_alpha_bb, - max_eigenvalue_for_alpha_bb=max_eigenvalue_for_alpha_bb, - eigenvalue_opt=eigenvalue_opt, - ) - lb = c.lower - ub = c.upper - parent_block.aux_cons.add(pe.inequality(lb, new_body, ub)) - parent_component = c.parent_component() - if parent_component.is_indexed(): - del parent_component[c.index()] - else: - parent_block.del_component(c) - - for c in nonrelaxation_component_data_objects( - m, ctype=pe.Objective, active=True, descend_into=descend_into, sort=True - ): - degree = polynomial_degree(c.expr) - if degree is not None: - if degree <= 1: - continue - - if c.sense == pe.minimize: - relaxation_side = RelaxationSide.UNDER - elif c.sense == pe.maximize: - relaxation_side = RelaxationSide.OVER - else: - raise ValueError( - 'Encountered an objective with an unrecognized sense: ' + str(c) - ) - - parent_block = c.parent_block() - - if parent_block in counter_dict: - counter = counter_dict[parent_block] - else: - parent_block.relaxations = pe.Block() - parent_block.aux_vars = pe.VarList() - parent_block.aux_cons = pe.ConstraintList() - counter = RelaxationCounter() - counter_dict[parent_block] = counter + new_body += _relax_expr( + expr=repn.nonlinear_expr, + aux_var_map=aux_var_map, + parent_block=m, + relaxation_side_map=relaxation_side_map, + counter=counter, + degree_map=degree_map, + ) + m.cons.add((cl, new_body, cu)) - if not hasattr(parent_block, 'aux_objectives'): - parent_block.aux_objectives = pe.ObjectiveList() + obj = get_objective(model) + if obj is not None: + degree = polynomial_degree(obj.expr) + if degree is None or degree > 1: + if obj.sense == pe.minimize: + relaxation_side = RelaxationSide.UNDER + elif obj.sense == pe.maximize: + relaxation_side = RelaxationSide.OVER + else: + raise ValueError( + 'Encountered an objective with an unrecognized sense: ' + str(obj) + ) - repn = generate_standard_repn(c.expr, quadratic=False, compute_values=False) - assert len(repn.quadratic_vars) == 0 - assert repn.nonlinear_expr is not None - if len(repn.linear_vars) > 0: - new_body = numeric_expr.LinearExpression( - constant=repn.constant, - linear_coefs=repn.linear_coefs, - linear_vars=repn.linear_vars, - ) - else: - new_body = repn.constant + repn = generate_standard_repn(obj.expr, quadratic=False, compute_values=True) + assert len(repn.quadratic_vars) == 0 + assert repn.nonlinear_expr is not None + if len(repn.linear_vars) > 0: + new_body = numeric_expr.LinearExpression( + constant=repn.constant, + linear_coefs=repn.linear_coefs, + linear_vars=repn.linear_vars, + ) + else: + new_body = repn.constant - relaxation_side_map = ComponentMap() - relaxation_side_map[repn.nonlinear_expr] = relaxation_side + relaxation_side_map = ComponentMap() + relaxation_side_map[repn.nonlinear_expr] = relaxation_side - if not use_alpha_bb: new_body += _relax_expr( expr=repn.nonlinear_expr, aux_var_map=aux_var_map, - parent_block=parent_block, - relaxation_side_map=relaxation_side_map, - counter=counter, - degree_map=degree_map, - ) - else: - new_body += _relax_expr_with_convexity_check( - orig_expr=repn.nonlinear_expr, - aux_var_map=aux_var_map, - parent_block=parent_block, + parent_block=m, relaxation_side_map=relaxation_side_map, counter=counter, degree_map=degree_map, - perform_expression_simplification=perform_expression_simplification, - eigenvalue_bounder=eigenvalue_bounder, - max_vars_per_alpha_bb=max_vars_per_alpha_bb, - max_eigenvalue_for_alpha_bb=max_eigenvalue_for_alpha_bb, - eigenvalue_opt=eigenvalue_opt, ) - sense = c.sense - parent_block.aux_objectives.add(new_body, sense=sense) - parent_component = c.parent_component() - if parent_component.is_indexed(): - del parent_component[c.index()] + m.obj = pe.Objective(expr=new_body, sense=obj.sense) else: - parent_block.del_component(c) - - if use_fbbt: - for relaxation in relaxation_data_objects(m, descend_into=True, active=True): - relaxation.rebuild(build_nonlinear_constraint=True) - - it = appsi.fbbt.IntervalTightener() - for k, v in fbbt_options.items(): - setattr(it.config, k, v) - it.config.deactivate_satisfied_constraints = False - it.perform_fbbt(m) + m.obj = pe.Objective(expr=obj.expr, sense=obj.sense) + + rel_list = list() + for r in relaxation_data_objects(model, descend_into=True, active=True): + rel_list.append(r) + + for r in rel_list: + var_map = ComponentMap() + for v in r.get_rhs_vars(): + if not v.is_fixed(): + all_vars.add(v) + var_map[v] = v + aux_var = r.get_aux_var() + var_map[aux_var] = aux_var + if not aux_var.is_fixed(): + all_vars.add(aux_var) + new_rel = copy_relaxation_with_local_data(r, var_map) + setattr(m, f'rel{counter}', new_rel) + counter.increment() - for relaxation in relaxation_data_objects(m, descend_into=True, active=True): - relaxation.use_linear_relaxation = True - relaxation.rebuild() - else: - for relaxation in relaxation_data_objects(m, descend_into=True, active=True): - relaxation.use_linear_relaxation = True - relaxation.rebuild() + for relaxation in relaxation_data_objects(m, descend_into=True, active=True): + relaxation.rebuild() return m diff --git a/pyomo/contrib/coramin/relaxations/relaxations_base.py b/pyomo/contrib/coramin/relaxations/relaxations_base.py index 71e0f0f6707..0fbe1258aca 100644 --- a/pyomo/contrib/coramin/relaxations/relaxations_base.py +++ b/pyomo/contrib/coramin/relaxations/relaxations_base.py @@ -66,12 +66,20 @@ def update( for v, val in zip(self.expr_vars, var_vals): v.set_value(val, skip_validation=True) try: - offset_val = pe.value(self.nonlin_expr) - for ndx, v in enumerate(self.expr_vars): - der = pe.value(self.derivs[ndx]) - offset_val -= der * v.value - self.coefficients[ndx]._value = der - self.offset._value = offset_val + offset_val = pe.value(self.nonlin_expr, exception=False) + if offset_val is None: + res = (False, None, None, 'evaluation error') + else: + for ndx, v in enumerate(self.expr_vars): + der = pe.value(self.derivs[ndx], exception=False) + if der is None: + res = (False, None, None, 'evaluation error') + break + else: + offset_val -= der * v.value + self.coefficients[ndx]._value = der + if res[0]: + self.offset._value = offset_val except (OverflowError, ValueError, ZeroDivisionError) as e: res = (False, None, None, str(e)) finally: @@ -305,7 +313,7 @@ def rebuild(self, build_nonlinear_constraint=False, ensure_oa_at_vertices=True): self._cuts = IndexedConstraint(pe.Any) if self._oa_params is None: del self._oa_params - self._oa_params = IndexedParam(pe.Any, mutable=True) + self._oa_params = IndexedParam(pe.Any, mutable=True, initialize=0, within=pe.Any) self.clean_oa_points(ensure_oa_at_vertices=ensure_oa_at_vertices) self._update_oa_cuts() else: @@ -439,10 +447,12 @@ def _get_oa_cut(self) -> _OACut: coef_params = list() for v in rhs_vars: p = self._oa_params[self._current_param_index] + p.value = None self._oa_param_indices[p] = self._current_param_index coef_params.append(p) self._current_param_index += 1 offset_param = self._oa_params[self._current_param_index] + offset_param.value = None self._oa_param_indices[offset_param] = self._current_param_index self._current_param_index += 1 oa_cut = _OACut(self._get_expr_for_oa(), rhs_vars, coef_params, offset_param) @@ -563,7 +573,7 @@ def clear_oa_points(self): self._current_param_index = 0 if self._oa_params is not None: del self._oa_params - self._oa_params = pe.Param(pe.Any, mutable=True) + self._oa_params = pe.Param(pe.Any, mutable=True, initialize=0, within=pe.Any) if self._cuts is not None: del self._cuts self._cuts = pe.Constraint(pe.Any) From 2e205a5d22b5077f76a2a34c1647b8af50afb6a2 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 26 Dec 2023 12:17:35 -0700 Subject: [PATCH 024/128] working on branch and bound --- pyomo/contrib/coramin/algorithms/bnb/bnb.py | 52 ++++++++++++++++++++- 1 file changed, 50 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/coramin/algorithms/bnb/bnb.py b/pyomo/contrib/coramin/algorithms/bnb/bnb.py index f9cd1152e09..8e416c75005 100644 --- a/pyomo/contrib/coramin/algorithms/bnb/bnb.py +++ b/pyomo/contrib/coramin/algorithms/bnb/bnb.py @@ -15,6 +15,8 @@ from pyomo.contrib.coramin.utils.pyomo_utils import get_objective from pyomo.core.base.block import _BlockData from pyomo.core.base.var import _GeneralVarData +from pyomo.contrib.coramin.heuristics.diving import run_diving_heuristic +from pyomo.contrib.coramin.domain_reduction.obbt import perform_obbt from typing import Tuple, List, Sequence import math import numpy as np @@ -131,7 +133,7 @@ def _fix_vars_with_close_bounds(m, tol=1e-12): class _BnB(pybnb.Problem): - def __init__(self, model: _BlockData, config: BnBConfig): + def __init__(self, model: _BlockData, config: BnBConfig, feasible_objective=None): # remove all parameters, fixed variables, etc. nlp, relaxation = clone_active_flat(model, 2) self.nlp: _BlockData = nlp @@ -178,6 +180,17 @@ def __init__(self, model: _BlockData, config: BnBConfig): self._sense = pybnb.maximize self.current_node: Optional[pybnb.Node] = None + self.feasible_objective = feasible_objective + + for iter in range(3): + perform_obbt( + relaxation, + solver=self.config.lp_solver, + varlist=list(self.rhs_vars), + objective_bound=feasible_objective, + ) + for r in self.relaxation_objects: + r.rebuild() def sense(self): return self._sense @@ -189,6 +202,39 @@ def bound(self): if res.termination_condition != appsi.base.TerminationCondition.optimal: raise RuntimeError(f"Cannot handle termination condition {res.termination_condition} when solving relaxation") res.solution_loader.load_vars() + + if self.current_node.tree_depth % 2 == 0 and self.current_node.tree_depth != 0: + should_obbt = True + if self._sense == pybnb.minimize: + if self.feasible_objective is None: + tmp = math.inf + else: + tmp = self.feasible_objective + feasible_objective = min(self.current_node.objective, tmp) + feasible_objective += abs(feasible_objective) * 1e-3 + 1e-3 + if feasible_objective - res.best_objective_bound <= self.config.rel_gap * feasible_objective + self.config.abs_gap: + should_obbt = False + else: + if self.feasible_objective is None: + tmp = -math.inf + else: + tmp = self.feasible_objective + feasible_objective = max(self.current_node.objective, tmp) + feasible_objective -= abs(feasible_objective) * 1e-3 + 1e-3 + if res.best_objective_bound - feasible_objective <= self.config.rel_gap * feasible_objective + self.config.abs_gap: + should_obbt = False + if not math.isfinite(feasible_objective): + feasible_objective = None + if should_obbt: + perform_obbt( + self.relaxation, + solver=self.config.lp_solver, + varlist=list(self.rhs_vars), + objective_bound=feasible_objective, + ) + for r in self.relaxation_objects: + r.rebuild() + return res.best_objective_bound def objective(self): @@ -309,9 +355,11 @@ def branch(self): def solve_with_bnb(model: _BlockData, config: BnBConfig, comm=None): # we don't want to modify the original model model, orig_var_map = _get_clone_and_var_map(model) - prob = _BnB(model, config) + diving_obj, diving_sol = run_diving_heuristic(model, config.nlp_solver, config.feasibility_tol, config.integer_tol) + prob = _BnB(model, config, feasible_objective=diving_obj) res = pybnb.solve( prob, + best_objective=diving_obj, queue_strategy=pybnb.QueueStrategy.bound, absolute_gap=config.abs_gap, relative_gap=config.rel_gap, From 9e6246960f1917eedd4c4ca916871d96ffa61007 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 26 Dec 2023 14:03:34 -0700 Subject: [PATCH 025/128] working on branch and bound --- pyomo/contrib/coramin/algorithms/bnb/bnb.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/coramin/algorithms/bnb/bnb.py b/pyomo/contrib/coramin/algorithms/bnb/bnb.py index 8e416c75005..697d8e79e7b 100644 --- a/pyomo/contrib/coramin/algorithms/bnb/bnb.py +++ b/pyomo/contrib/coramin/algorithms/bnb/bnb.py @@ -203,6 +203,22 @@ def bound(self): raise RuntimeError(f"Cannot handle termination condition {res.termination_condition} when solving relaxation") res.solution_loader.load_vars() + while True: + added_cuts = False + for r in self.relaxation_objects: + new_con = r.add_cut(keep_cut=True, check_violation=True, feasibility_tol=1e-8) + if new_con is not None: + added_cuts = True + if added_cuts: + res = self.config.lp_solver.solve(self.relaxation) + if res.termination_condition == appsi.base.TerminationCondition.infeasible: + return self.infeasible_objective() + if res.termination_condition != appsi.base.TerminationCondition.optimal: + raise RuntimeError(f"Cannot handle termination condition {res.termination_condition} when solving relaxation") + res.solution_loader.load_vars() + else: + break + if self.current_node.tree_depth % 2 == 0 and self.current_node.tree_depth != 0: should_obbt = True if self._sense == pybnb.minimize: @@ -328,6 +344,7 @@ def branch(self): max_viol = err if var_to_branch_on is None: + return pybnb.Node() raise NotImplementedError("relaxation was feasible - add handling for this") xl1 = xl.copy() @@ -363,7 +380,7 @@ def solve_with_bnb(model: _BlockData, config: BnBConfig, comm=None): queue_strategy=pybnb.QueueStrategy.bound, absolute_gap=config.abs_gap, relative_gap=config.rel_gap, - comparison_tolerance=1e-5, + comparison_tolerance=1e-4, comm=comm, # log=logger, ) From c803f56fd930ea2a66a022c11dd3db4db7caf1bf Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 27 Dec 2023 21:19:44 -0700 Subject: [PATCH 026/128] branch and bound improvements --- pyomo/contrib/coramin/algorithms/bnb/bnb.py | 27 ++++++++++++------- .../coramin/cutting_planes/alpha_bb_cuts.py | 2 +- pyomo/contrib/coramin/relaxations/hessian.py | 8 +----- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/pyomo/contrib/coramin/algorithms/bnb/bnb.py b/pyomo/contrib/coramin/algorithms/bnb/bnb.py index 697d8e79e7b..b3c1152b5b4 100644 --- a/pyomo/contrib/coramin/algorithms/bnb/bnb.py +++ b/pyomo/contrib/coramin/algorithms/bnb/bnb.py @@ -143,13 +143,25 @@ def __init__(self, model: _BlockData, config: BnBConfig, feasible_objective=None self.config.lp_solver.update_config.treat_fixed_vars_as_params = True self.config.nlp_solver.config.load_solution = False + obj = get_objective(nlp) + if obj.sense == pe.minimize: + self._sense = pybnb.minimize + else: + self._sense = pybnb.maximize + # perform fbbt before constructing relaxations in case # we can identify things like x**3 is convex because # x >= 0 self.interval_tightener = it = appsi.fbbt.IntervalTightener() it.config.deactivate_satisfied_constraints = True it.config.feasibility_tol = config.feasibility_tol + if feasible_objective is not None: + if obj.sense == pe.minimize: + relaxation.obj_ineq = pe.Constraint(expr=obj.expr <= feasible_objective) + else: + relaxation.obj_ineq = pe.Constraint(expr=obj.expr >= feasible_objective) it.perform_fbbt(relaxation) + del relaxation.obj_ineq _fix_vars_with_close_bounds(relaxation) impose_structure(relaxation) @@ -174,11 +186,6 @@ def __init__(self, model: _BlockData, config: BnBConfig, feasible_objective=None self.all_branching_vars = list(binary_vars) + list(integer_vars) + list(self.rhs_vars) self.var_to_ndx_map = ComponentMap((v, ndx) for ndx, v in enumerate(self.all_branching_vars)) - if get_objective(nlp).sense == pe.minimize: - self._sense = pybnb.minimize - else: - self._sense = pybnb.maximize - self.current_node: Optional[pybnb.Node] = None self.feasible_objective = feasible_objective @@ -206,7 +213,7 @@ def bound(self): while True: added_cuts = False for r in self.relaxation_objects: - new_con = r.add_cut(keep_cut=True, check_violation=True, feasibility_tol=1e-8) + new_con = r.add_cut(keep_cut=True, check_violation=True, feasibility_tol=self.config.feasibility_tol) if new_con is not None: added_cuts = True if added_cuts: @@ -317,10 +324,10 @@ def load_state(self, node): else: v.setub(None) - if lb == ub: - v.fix(lb) - else: - v.unfix() + if lb == ub: + v.fix(lb) + else: + v.unfix() for r in self.relaxation_objects: r.rebuild() diff --git a/pyomo/contrib/coramin/cutting_planes/alpha_bb_cuts.py b/pyomo/contrib/coramin/cutting_planes/alpha_bb_cuts.py index 1587adc8482..54bf6999276 100644 --- a/pyomo/contrib/coramin/cutting_planes/alpha_bb_cuts.py +++ b/pyomo/contrib/coramin/cutting_planes/alpha_bb_cuts.py @@ -24,7 +24,7 @@ def generate(self, model: _BlockData, solver: Solver | None = None) -> Optional[ if lhs_val >= value(self.rhs): return None - alpha = max(0, -0.5 * self.hessian.get_maximum_eigenvalue()) + alpha = max(0, -0.5 * self.hessian.get_minimum_eigenvalue()) alpha_sum = 0 for ndx, v in enumerate(self.xlist): lb, ub = v.bounds diff --git a/pyomo/contrib/coramin/relaxations/hessian.py b/pyomo/contrib/coramin/relaxations/hessian.py index 1bf3bdcedb1..16b1c1a8e98 100644 --- a/pyomo/contrib/coramin/relaxations/hessian.py +++ b/pyomo/contrib/coramin/relaxations/hessian.py @@ -123,15 +123,9 @@ def formulate_eigenvalue_relaxation(self, sense=pe.minimize): all_vars = list( ComponentSet(m.component_data_objects(pe.Var, descend_into=True)) ) - tmp_name = unique_component_name(m, "all_vars") - setattr(m, tmp_name, all_vars) from .auto_relax import relax - relaxation = relax(m, in_place=False) - new_vars = getattr(relaxation, "all_vars") - self._orig_to_relaxation_vars = pe.ComponentMap(zip(all_vars, new_vars)) - delattr(m, tmp_name) - delattr(relaxation, tmp_name) + relaxation = relax(m) self._eigenvalue_relaxation = relaxation return relaxation From 5840de0baa2fb5ad97334e3d6dc967626e10b7c2 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 9 Jan 2024 15:57:24 -0700 Subject: [PATCH 027/128] BnB improvements --- pyomo/contrib/appsi/solvers/highs.py | 1 + pyomo/contrib/coramin/algorithms/bnb/bnb.py | 185 +++++++++++++----- .../contrib/coramin/relaxations/auto_relax.py | 3 +- 3 files changed, 139 insertions(+), 50 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/highs.py b/pyomo/contrib/appsi/solvers/highs.py index 3d498f9388e..9cf26779763 100644 --- a/pyomo/contrib/appsi/solvers/highs.py +++ b/pyomo/contrib/appsi/solvers/highs.py @@ -461,6 +461,7 @@ def _remove_constraints(self, cons: List[_GeneralConstraintData]): del self._solver_con_to_pyomo_con_map[con_ndx] indices_to_remove.append(con_ndx) self._mutable_helpers.pop(con, None) + indices_to_remove.sort() self._solver_model.deleteRows( len(indices_to_remove), np.array(indices_to_remove) ) diff --git a/pyomo/contrib/coramin/algorithms/bnb/bnb.py b/pyomo/contrib/coramin/algorithms/bnb/bnb.py index b3c1152b5b4..0230a9e84df 100644 --- a/pyomo/contrib/coramin/algorithms/bnb/bnb.py +++ b/pyomo/contrib/coramin/algorithms/bnb/bnb.py @@ -1,4 +1,5 @@ import pybnb +from pyomo.common.timing import HierarchicalTimer from pyomo.core.base.block import _BlockData from pyomo.common.modeling import unique_component_name import pyomo.environ as pe @@ -21,6 +22,15 @@ import math import numpy as np import logging +from pyomo.contrib.appsi.base import ( + Solver, + MIPSolverConfig, + Results, + TerminationCondition, + SolutionLoader, + SolverFactory, +) +from pyomo.core.staleflag import StaleFlagManager logger = logging.getLogger(__name__) @@ -51,42 +61,28 @@ def _get_clone_and_var_map(m1: _BlockData): return m2, var_map -class BnBConfig(ConfigDict): - def __init__( - self, - description=None, - doc=None, - implicit=False, - implicit_domain=None, - visibility=0, - ): - super().__init__(description, doc, implicit, implicit_domain, visibility) +class BnBConfig(MIPSolverConfig): + def __init__(self): + super().__init__(None, None, False, None, 0) self.feasibility_tol = self.declare( "feasibility_tol", ConfigValue(domain=PositiveFloat, default=1e-8) ) self.lp_solver = self.declare("lp_solver", ConfigValue()) self.nlp_solver = self.declare("nlp_solver", ConfigValue()) self.abs_gap = self.declare("abs_gap", ConfigValue(default=1e-4)) - self.rel_gap = self.declare("rel_gap", ConfigValue(default=1e-3)) self.integer_tol = self.declare("integer_tol", ConfigValue(default=1e-4)) + self.node_limit = self.declare("node_limit", ConfigValue(default=1000000000)) + self.mip_gap = 1e-3 -def collect_vars(m: _BlockData) -> Tuple[List[_GeneralVarData], List[_GeneralVarData], List[_GeneralVarData]]: +def collect_vars(m: _BlockData) -> Tuple[List[_GeneralVarData], List[_GeneralVarData]]: binary_vars = ComponentSet() integer_vars = ComponentSet() - for c in m.component_data_objects(pe.Constraint, active=True, descend_into=pe.Block): - for v in identify_variables(c.body, include_fixed=False): - if v.is_binary(): - binary_vars.add(v) - elif v.is_integer(): - integer_vars.add(v) - obj = get_objective(m) - if obj is not None: - for v in identify_variables(obj.expr, include_fixed=False): - if v.is_binary(): - binary_vars.add(v) - elif v.is_integer(): - integer_vars.add(v) + for v in m.vars: + if v.is_binary(): + binary_vars.add(v) + elif v.is_integer(): + integer_vars.add(v) return list(binary_vars), list(integer_vars) @@ -99,7 +95,7 @@ def relax_integers(binary_vars: Sequence[_GeneralVarData], integer_vars: Sequenc def impose_structure(m): - m.aux = pe.VarList() + m.aux_vars = pe.VarList() for key, c in list(m.nonlinear.cons.items()): repn: StandardRepn = generate_standard_repn(c.body, quadratic=False, compute_values=True) @@ -110,7 +106,7 @@ def impose_structure(m): linear_coefs = list(repn.linear_coefs) linear_vars = list(repn.linear_vars) for term in expr_list: - v = m.aux.add() + v = m.aux_vars.add() linear_coefs.append(1) linear_vars.append(v) m.vars.append(v) @@ -128,7 +124,7 @@ def _fix_vars_with_close_bounds(m, tol=1e-12): lb, ub = v.bounds if lb is None or ub is None: continue - if abs(ub - lb) <= tol * min(abs(lb), abs(ub)): + if abs(ub - lb) <= tol * min(abs(lb), abs(ub)) + tol: v.fix(0.5 * (lb + ub)) @@ -183,8 +179,14 @@ def __init__(self, model: _BlockData, config: BnBConfig, feasible_objective=None self.rhs_vars.extend(i for i in r.get_rhs_vars() if not i.is_fixed()) self.rhs_vars = list(ComponentSet(self.rhs_vars) - int_var_set) + var_set = ComponentSet(self.binary_vars + self.integer_vars + self.rhs_vars) + other_vars = ComponentSet(i for i in nlp.vars if i not in var_set) + other_vars.update(i for i in relaxation.aux_vars.values() if i not in var_set) + self.other_vars = other_vars = list(other_vars) + self.all_branching_vars = list(binary_vars) + list(integer_vars) + list(self.rhs_vars) - self.var_to_ndx_map = ComponentMap((v, ndx) for ndx, v in enumerate(self.all_branching_vars)) + self.all_vars = self.all_branching_vars + self.other_vars + self.var_to_ndx_map = ComponentMap((v, ndx) for ndx, v in enumerate(self.all_vars)) self.current_node: Optional[pybnb.Node] = None self.feasible_objective = feasible_objective @@ -226,25 +228,40 @@ def bound(self): else: break + # if the solution is feasible, we are done + is_feasible = True + for v in self.bin_and_int_vars: + err = abs(v.value - round(v.value)) + if err > self.config.integer_tol: + is_feasible = False + break + if is_feasible: + for r in self.relaxation_objects: + err = r.get_deviation() + if err > self.config.feasibility_tol: + is_feasible = False + break + if is_feasible: + return res.best_feasible_objective + + # maybe do OBBT if self.current_node.tree_depth % 2 == 0 and self.current_node.tree_depth != 0: should_obbt = True if self._sense == pybnb.minimize: if self.feasible_objective is None: - tmp = math.inf + feasible_objective = math.inf else: - tmp = self.feasible_objective - feasible_objective = min(self.current_node.objective, tmp) + feasible_objective = self.feasible_objective feasible_objective += abs(feasible_objective) * 1e-3 + 1e-3 - if feasible_objective - res.best_objective_bound <= self.config.rel_gap * feasible_objective + self.config.abs_gap: + if feasible_objective - res.best_objective_bound <= self.config.mip_gap * feasible_objective + self.config.abs_gap: should_obbt = False else: if self.feasible_objective is None: - tmp = -math.inf + feasible_objective = -math.inf else: - tmp = self.feasible_objective - feasible_objective = max(self.current_node.objective, tmp) + feasible_objective = self.feasible_objective feasible_objective -= abs(feasible_objective) * 1e-3 + 1e-3 - if res.best_objective_bound - feasible_objective <= self.config.rel_gap * feasible_objective + self.config.abs_gap: + if res.best_objective_bound - feasible_objective <= self.config.mip_gap * feasible_objective + self.config.abs_gap: should_obbt = False if not math.isfinite(feasible_objective): feasible_objective = None @@ -261,6 +278,8 @@ def bound(self): return res.best_objective_bound def objective(self): + if self.current_node.state[2] is not None: + return self.current_node.state[3] if self.current_node.tree_depth % 10 != 0: return self.infeasible_objective() unfixed_vars = [v for v in self.bin_and_int_vars if not v.is_fixed()] @@ -272,6 +291,17 @@ def objective(self): ret = self.infeasible_objective() else: ret = res.best_feasible_objective + if self.sense == pybnb.minimize: + if ret < self.feasible_objective: + self.feasible_objective = ret + else: + if ret > self.feasible_objective: + self.feasible_objective = ret + res.solution_loader.load_vars() + orig_vars = ComponentSet(self.nlp.vars) + sol = np.array([v.value for v in self.all_vars], dtype=float) + xl, xu, _, _ = self.current_node.state + self.current_node.state = (xl, xu, sol, ret) for v, val in zip(unfixed_vars, vals): v.unfix() # we have to restore the values so that branch() works properly @@ -286,7 +316,7 @@ def get_state(self): xl.append(math.ceil(v.lb)) xu.append(math.floor(v.ub)) - for v in self.rhs_vars: + for v in self.rhs_vars + self.other_vars: lb, ub = v.bounds if lb is None: xl.append(-math.inf) @@ -300,21 +330,19 @@ def get_state(self): xl = np.array(xl, dtype=float) xu = np.array(xu, dtype=float) - return xl, xu + return xl, xu, None, None def save_state(self, node): node.state = self.get_state() def load_state(self, node): self.current_node = node - xl, xu = node.state + xl, xu, _, _ = node.state xl = [float(i) for i in xl] xu = [float(i) for i in xu] - all_vars = self.all_branching_vars - - for v, lb, ub in zip(all_vars, xl, xu): + for v, lb, ub in zip(self.all_vars, xl, xu): if math.isfinite(lb): v.setlb(lb) else: @@ -333,7 +361,7 @@ def load_state(self, node): r.rebuild() def branch(self): - xl, xu = self.get_state() + xl, xu, _, _ = self.get_state() var_to_branch_on = None max_viol = 0 @@ -351,8 +379,9 @@ def branch(self): max_viol = err if var_to_branch_on is None: + # the relaxation was feasible + # no nodes in this part of the tree need explored return pybnb.Node() - raise NotImplementedError("relaxation was feasible - add handling for this") xl1 = xl.copy() xu1 = xu.copy() @@ -369,8 +398,8 @@ def branch(self): xu1[ndx_to_branch_on] = new_ub xl2[ndx_to_branch_on] = new_lb - child1.state = (xl1, xu1) - child2.state = (xl2, xu2) + child1.state = (xl1, xu1, None, None) + child2.state = (xl2, xu2, None, None) yield child1 yield child2 @@ -381,13 +410,71 @@ def solve_with_bnb(model: _BlockData, config: BnBConfig, comm=None): model, orig_var_map = _get_clone_and_var_map(model) diving_obj, diving_sol = run_diving_heuristic(model, config.nlp_solver, config.feasibility_tol, config.integer_tol) prob = _BnB(model, config, feasible_objective=diving_obj) - res = pybnb.solve( + res: pybnb.SolverResults = pybnb.solve( prob, best_objective=diving_obj, queue_strategy=pybnb.QueueStrategy.bound, absolute_gap=config.abs_gap, - relative_gap=config.rel_gap, + relative_gap=config.mip_gap, comparison_tolerance=1e-4, comm=comm, + time_limit=config.time_limit, + node_limit=config.node_limit, # log=logger, ) + ret = Results() + ret.best_feasible_objective = res.objective + ret.best_objective_bound = res.bound + ss = pybnb.SolutionStatus + if res.solution_status == ss.optimal: + ret.termination_condition = TerminationCondition.optimal + elif res.solution_status == ss.infeasible: + ret.termination_condition = TerminationCondition.infeasible + elif res.solution_status == ss.unbounded: + ret.termination_condition = TerminationCondition.unbounded + else: + ret.termination_condition = TerminationCondition.unknown + best_node = res.best_node + if best_node is None: + if diving_obj is not None: + ret.solution_loader = SolutionLoader(primals={id(orig_var_map[v]): (orig_var_map[v], val) for v, val in diving_sol.items()}, duals=None, slacks=None, reduced_costs=None) + else: + vals = best_node.state[2] + primals = dict() + orig_vars = ComponentSet(prob.nlp.vars) + for v, val in zip(prob.all_vars, vals): + if v in orig_vars: + ov = orig_var_map[v] + primals[id(ov)] = (ov, val) + ret.solution_loader = SolutionLoader(primals=primals, duals=None, slacks=None, reduced_costs=None) + return ret + + +class BnBSolver(Solver): + def __init__(self) -> None: + super().__init__() + self._config = BnBConfig() + + def available(self): + return self.Availability.FullLicense + + def version(self) -> Tuple: + return (1, 0, 0) + + @property + def config(self): + return self._config + + @property + def symbol_map(self): + raise NotImplementedError('do this') + + def solve(self, model: _BlockData, timer: HierarchicalTimer = None) -> Results: + StaleFlagManager.mark_all_as_stale() + res = solve_with_bnb(model, self.config) + if self.config.load_solution: + res.solution_loader.load_vars() + return res + + +SolverFactory.register(name="coramin_bnb", doc="Coramin Branch and Bound Solver")(BnBSolver) diff --git a/pyomo/contrib/coramin/relaxations/auto_relax.py b/pyomo/contrib/coramin/relaxations/auto_relax.py index 9bea8293a91..85ae2f74d84 100644 --- a/pyomo/contrib/coramin/relaxations/auto_relax.py +++ b/pyomo/contrib/coramin/relaxations/auto_relax.py @@ -1482,7 +1482,8 @@ def relax( """ m = pe.Block(concrete=True) m.cons = pe.ConstraintList() - m.aux_vars = pe.VarList() + if not hasattr(m, 'aux_vars'): + m.aux_vars = pe.VarList() m.relaxations = pe.Block() m.aux_cons = pe.ConstraintList() From 475ec06fb243b9afa4f59a8cefbbacd56d6634f3 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 10:35:33 -0700 Subject: [PATCH 028/128] simplification tests --- .../simplification/ginac_interface.cpp | 2 +- pyomo/contrib/simplification/simplify.py | 5 +- .../tests/test_simplification.py | 79 ++++++++++++++++--- 3 files changed, 73 insertions(+), 13 deletions(-) diff --git a/pyomo/contrib/simplification/ginac_interface.cpp b/pyomo/contrib/simplification/ginac_interface.cpp index 690885dc513..32bea8dadd0 100644 --- a/pyomo/contrib/simplification/ginac_interface.cpp +++ b/pyomo/contrib/simplification/ginac_interface.cpp @@ -21,7 +21,7 @@ ex ginac_expr_from_pyomo_node( case py_float: { double val = expr.cast(); if (is_integer(val)) { - res = numeric(expr.cast()); + res = numeric((long) val); } else { res = numeric(val); diff --git a/pyomo/contrib/simplification/simplify.py b/pyomo/contrib/simplification/simplify.py index 938bff6b4b9..66a3dad0b06 100644 --- a/pyomo/contrib/simplification/simplify.py +++ b/pyomo/contrib/simplification/simplify.py @@ -25,7 +25,10 @@ def simplify_with_sympy(expr: NumericExpression): def simplify_with_ginac(expr: NumericExpression, ginac_interface): gi = ginac_interface - return gi.from_ginac(gi.to_ginac(expr).normal()) + ginac_expr = gi.to_ginac(expr) + ginac_expr = ginac_expr.normal() + new_expr = gi.from_ginac(ginac_expr) + return new_expr class Simplifier(object): diff --git a/pyomo/contrib/simplification/tests/test_simplification.py b/pyomo/contrib/simplification/tests/test_simplification.py index 02107ba1d6c..4d9b0cec0d2 100644 --- a/pyomo/contrib/simplification/tests/test_simplification.py +++ b/pyomo/contrib/simplification/tests/test_simplification.py @@ -6,6 +6,14 @@ class TestSimplification(TestCase): + def compare_against_possible_results(self, got, expected_list): + success = False + for exp in expected_list: + if compare_expressions(got, exp): + success = True + break + self.assertTrue(success) + def test_simplify(self): m = pe.ConcreteModel() x = m.x = pe.Var(bounds=(0, None)) @@ -24,13 +32,16 @@ def test_param(self): e1 = p*x**2 + p*x + p*x**2 simp = Simplifier() e2 = simp.simplify(e1) - exp1 = p*x**2.0*2.0 + p*x - exp2 = p*x + p*x**2.0*2.0 - self.assertTrue( - compare_expressions(e2, exp1) - or compare_expressions(e2, exp2) - or compare_expressions(e2, p*x + x**2.0*p*2.0) - or compare_expressions(e2, x**2.0*p*2.0 + p*x) + self.compare_against_possible_results( + e2, + [ + p*x**2.0*2.0 + p*x, + p*x + p*x**2.0*2.0, + 2.0*p*x**2.0 + p*x, + p*x + 2.0*p*x**2.0, + x**2.0*p*2.0 + p*x, + p*x + x**2.0*p*2.0 + ] ) def test_mul(self): @@ -48,8 +59,13 @@ def test_sum(self): e = 2 + x simp = Simplifier() e2 = simp.simplify(e) - expected = x + 2.0 - assertExpressionsEqual(self, expected, e2) + self.compare_against_possible_results( + e2, + [ + 2.0 + x, + x + 2.0, + ] + ) def test_neg(self): m = pe.ConcreteModel() @@ -57,6 +73,47 @@ def test_neg(self): e = -pe.log(x) simp = Simplifier() e2 = simp.simplify(e) - expected = pe.log(x)*(-1.0) - assertExpressionsEqual(self, expected, e2) + self.compare_against_possible_results( + e2, + [ + (-1.0)*pe.log(x), + pe.log(x)*(-1.0), + -pe.log(x), + ] + ) + + def test_pow(self): + m = pe.ConcreteModel() + x = m.x = pe.Var() + e = x**2.0 + simp = Simplifier() + e2 = simp.simplify(e) + assertExpressionsEqual(self, e, e2) + def test_div(self): + m = pe.ConcreteModel() + x = m.x = pe.Var() + y = m.y = pe.Var() + e = x/y + y/x - x/y + simp = Simplifier() + e2 = simp.simplify(e) + print(e2) + self.compare_against_possible_results( + e2, + [ + y/x, + y*(1.0/x), + y*x**-1.0, + x**-1.0 * y, + ], + ) + + def test_unary(self): + m = pe.ConcreteModel() + x = m.x = pe.Var() + func_list = [pe.log, pe.sin, pe.cos, pe.tan, pe.asin, pe.acos, pe.atan] + for func in func_list: + e = func(x) + simp = Simplifier() + e2 = simp.simplify(e) + assertExpressionsEqual(self, e, e2) From 491db9f6793dc6d84fc3b771073d00d938b3a2f2 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 15:57:34 -0700 Subject: [PATCH 029/128] update GHA to install ginac --- .github/workflows/test_pr_and_main.yml | 21 ++++++++++++++++++- .../tests/test_simplification.py | 2 ++ setup.cfg | 1 + 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index 2885fd107a8..12dc7c1daac 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -98,7 +98,7 @@ jobs: - os: ubuntu-latest python: 3.11 other: /singletest - category: "-m 'neos or importtest'" + category: "-m 'neos or importtest or simplification'" skip_doctest: 1 TARGET: linux PYENV: pip @@ -179,6 +179,25 @@ jobs: # path: cache/os # key: pkg-${{env.CACHE_VER}}.0-${{runner.os}} + - name: install ginac + if: ${{ matrix.other == "singletest" }} + run: | + pwd + cd .. + curl https://www.ginac.de/CLN/cln-1.3.6.tar.bz2 >cln-1.3.6.tar.bz2 + tar -xvf cln-1.3.6.tar.bz2 + cd cln-1.3.6 + ./configure + make + make install + cd + curl https://www.ginac.de/ginac-1.8.7.tar.bz2 >ginac-1.8.7.tar.bz2 + tar -xvf ginac-1.8.7.tar.bz2 + cd ginac-1.8.7 + ./configure + make + make install + - name: TPL package download cache uses: actions/cache@v3 if: ${{ ! matrix.slim }} diff --git a/pyomo/contrib/simplification/tests/test_simplification.py b/pyomo/contrib/simplification/tests/test_simplification.py index 4d9b0cec0d2..ed59064022c 100644 --- a/pyomo/contrib/simplification/tests/test_simplification.py +++ b/pyomo/contrib/simplification/tests/test_simplification.py @@ -1,10 +1,12 @@ from pyomo.common.unittest import TestCase +from pyomo.common import unittest from pyomo.contrib.simplification import Simplifier from pyomo.core.expr.compare import assertExpressionsEqual, compare_expressions import pyomo.environ as pe from pyomo.core.expr.calculus.diff_with_pyomo import reverse_sd +@unittest.pytest.mark.simplification class TestSimplification(TestCase): def compare_against_possible_results(self, got, expected_list): success = False diff --git a/setup.cfg b/setup.cfg index b606138f38c..a431e0cd601 100644 --- a/setup.cfg +++ b/setup.cfg @@ -22,3 +22,4 @@ markers = lp: marks lp tests gams: marks gams tests bar: marks bar tests + simplification: marks simplification tests that have expensive (to install) dependencies From b3a1ff9b06e3fb2e8fd944bb27d2bf736a9cfd54 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 16:02:28 -0700 Subject: [PATCH 030/128] run black --- pyomo/contrib/simplification/build.py | 16 +++--- pyomo/contrib/simplification/simplify.py | 2 + .../tests/test_simplification.py | 49 ++++++------------- 3 files changed, 27 insertions(+), 40 deletions(-) diff --git a/pyomo/contrib/simplification/build.py b/pyomo/contrib/simplification/build.py index 6f16607e22b..e8bd645756b 100644 --- a/pyomo/contrib/simplification/build.py +++ b/pyomo/contrib/simplification/build.py @@ -11,16 +11,16 @@ def build_ginac_interface(args=[]): dname = this_file_dir() - _sources = [ - 'ginac_interface.cpp', - ] + _sources = ['ginac_interface.cpp'] sources = list() for fname in _sources: sources.append(os.path.join(dname, fname)) ginac_lib = find_library('ginac') if ginac_lib is None: - raise RuntimeError('could not find GiNaC library; please make sure it is in the LD_LIBRARY_PATH environment variable') + raise RuntimeError( + 'could not find GiNaC library; please make sure it is in the LD_LIBRARY_PATH environment variable' + ) ginac_lib_dir = os.path.dirname(ginac_lib) ginac_build_dir = os.path.dirname(ginac_lib_dir) ginac_include_dir = os.path.join(ginac_build_dir, 'include') @@ -29,7 +29,9 @@ def build_ginac_interface(args=[]): cln_lib = find_library('cln') if cln_lib is None: - raise RuntimeError('could not find CLN library; please make sure it is in the LD_LIBRARY_PATH environment variable') + raise RuntimeError( + 'could not find CLN library; please make sure it is in the LD_LIBRARY_PATH environment variable' + ) cln_lib_dir = os.path.dirname(cln_lib) cln_build_dir = os.path.dirname(cln_lib_dir) cln_include_dir = os.path.join(cln_build_dir, 'include') @@ -38,8 +40,8 @@ def build_ginac_interface(args=[]): extra_args = ['-std=c++11'] ext = Pybind11Extension( - 'ginac_interface', - sources=sources, + 'ginac_interface', + sources=sources, language='c++', include_dirs=[cln_include_dir, ginac_include_dir], library_dirs=[cln_lib_dir, ginac_lib_dir], diff --git a/pyomo/contrib/simplification/simplify.py b/pyomo/contrib/simplification/simplify.py index 66a3dad0b06..8f7f15f3826 100644 --- a/pyomo/contrib/simplification/simplify.py +++ b/pyomo/contrib/simplification/simplify.py @@ -3,8 +3,10 @@ from pyomo.core.expr.numvalue import is_fixed, value import logging import warnings + try: from pyomo.contrib.simplification.ginac_interface import GinacInterface + ginac_available = True except: GinacInterface = None diff --git a/pyomo/contrib/simplification/tests/test_simplification.py b/pyomo/contrib/simplification/tests/test_simplification.py index ed59064022c..cc278db4d43 100644 --- a/pyomo/contrib/simplification/tests/test_simplification.py +++ b/pyomo/contrib/simplification/tests/test_simplification.py @@ -19,7 +19,7 @@ def compare_against_possible_results(self, got, expected_list): def test_simplify(self): m = pe.ConcreteModel() x = m.x = pe.Var(bounds=(0, None)) - e = x*pe.log(x) + e = x * pe.log(x) der1 = reverse_sd(e)[x] der2 = reverse_sd(der1)[x] simp = Simplifier() @@ -31,28 +31,28 @@ def test_param(self): m = pe.ConcreteModel() x = m.x = pe.Var() p = m.p = pe.Param(mutable=True) - e1 = p*x**2 + p*x + p*x**2 + e1 = p * x**2 + p * x + p * x**2 simp = Simplifier() e2 = simp.simplify(e1) self.compare_against_possible_results( - e2, + e2, [ - p*x**2.0*2.0 + p*x, - p*x + p*x**2.0*2.0, - 2.0*p*x**2.0 + p*x, - p*x + 2.0*p*x**2.0, - x**2.0*p*2.0 + p*x, - p*x + x**2.0*p*2.0 - ] + p * x**2.0 * 2.0 + p * x, + p * x + p * x**2.0 * 2.0, + 2.0 * p * x**2.0 + p * x, + p * x + 2.0 * p * x**2.0, + x**2.0 * p * 2.0 + p * x, + p * x + x**2.0 * p * 2.0, + ], ) def test_mul(self): m = pe.ConcreteModel() x = m.x = pe.Var() - e = 2*x + e = 2 * x simp = Simplifier() e2 = simp.simplify(e) - expected = 2.0*x + expected = 2.0 * x assertExpressionsEqual(self, expected, e2) def test_sum(self): @@ -61,13 +61,7 @@ def test_sum(self): e = 2 + x simp = Simplifier() e2 = simp.simplify(e) - self.compare_against_possible_results( - e2, - [ - 2.0 + x, - x + 2.0, - ] - ) + self.compare_against_possible_results(e2, [2.0 + x, x + 2.0]) def test_neg(self): m = pe.ConcreteModel() @@ -76,12 +70,7 @@ def test_neg(self): simp = Simplifier() e2 = simp.simplify(e) self.compare_against_possible_results( - e2, - [ - (-1.0)*pe.log(x), - pe.log(x)*(-1.0), - -pe.log(x), - ] + e2, [(-1.0) * pe.log(x), pe.log(x) * (-1.0), -pe.log(x)] ) def test_pow(self): @@ -96,18 +85,12 @@ def test_div(self): m = pe.ConcreteModel() x = m.x = pe.Var() y = m.y = pe.Var() - e = x/y + y/x - x/y + e = x / y + y / x - x / y simp = Simplifier() e2 = simp.simplify(e) print(e2) self.compare_against_possible_results( - e2, - [ - y/x, - y*(1.0/x), - y*x**-1.0, - x**-1.0 * y, - ], + e2, [y / x, y * (1.0 / x), y * x**-1.0, x**-1.0 * y] ) def test_unary(self): From de8743a84c4902867a802756ab64fbd62eed920e Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 16:03:42 -0700 Subject: [PATCH 031/128] syntax --- .github/workflows/test_pr_and_main.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index 12dc7c1daac..7345fd45e10 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -180,7 +180,7 @@ jobs: # key: pkg-${{env.CACHE_VER}}.0-${{runner.os}} - name: install ginac - if: ${{ matrix.other == "singletest" }} + if: ${{ matrix.other == 'singletest' }} run: | pwd cd .. From ee9f830984ad20446b28c8aa65674d5cbf264ab3 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 16:07:25 -0700 Subject: [PATCH 032/128] install ginac in GHA --- .github/workflows/test_branches.yml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index e773587ec85..3270b3e8a95 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -94,6 +94,14 @@ jobs: PYENV: conda PACKAGES: mpi4py + - os: ubuntu-latest + python: 3.11 + other: /singletest + category: "-m 'neos or importtest or simplification'" + skip_doctest: 1 + TARGET: linux + PYENV: pip + - os: ubuntu-latest python: '3.10' other: /cython @@ -149,6 +157,25 @@ jobs: # path: cache/os # key: pkg-${{env.CACHE_VER}}.0-${{runner.os}} + - name: install ginac + if: ${{ matrix.other == 'singletest' }} + run: | + pwd + cd .. + curl https://www.ginac.de/CLN/cln-1.3.6.tar.bz2 >cln-1.3.6.tar.bz2 + tar -xvf cln-1.3.6.tar.bz2 + cd cln-1.3.6 + ./configure + make + make install + cd + curl https://www.ginac.de/ginac-1.8.7.tar.bz2 >ginac-1.8.7.tar.bz2 + tar -xvf ginac-1.8.7.tar.bz2 + cd ginac-1.8.7 + ./configure + make + make install + - name: TPL package download cache uses: actions/cache@v3 if: ${{ ! matrix.slim }} From 36cfd6388d16d6f2077d51b9035c9e363b4e4e28 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 16:09:53 -0700 Subject: [PATCH 033/128] install ginac in GHA --- .github/workflows/test_branches.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 3270b3e8a95..97b9c6ed1dc 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -158,7 +158,7 @@ jobs: # key: pkg-${{env.CACHE_VER}}.0-${{runner.os}} - name: install ginac - if: ${{ matrix.other == 'singletest' }} + if: matrix.TARGET == 'singletest' run: | pwd cd .. From 8269539ff67b48e4c7b652b8feabb14c64634fc6 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 16:11:27 -0700 Subject: [PATCH 034/128] install ginac in GHA --- .github/workflows/test_branches.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 97b9c6ed1dc..c56ec398134 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -158,7 +158,7 @@ jobs: # key: pkg-${{env.CACHE_VER}}.0-${{runner.os}} - name: install ginac - if: matrix.TARGET == 'singletest' + if: matrix.other == 'singletest' run: | pwd cd .. From 7bb0ff501344dfc9bb162f9ed339293ad320d0d1 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 16:14:07 -0700 Subject: [PATCH 035/128] install ginac in GHA --- .github/workflows/test_branches.yml | 2 +- pyomo/contrib/simplification/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index c56ec398134..bea57f314e5 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -158,7 +158,7 @@ jobs: # key: pkg-${{env.CACHE_VER}}.0-${{runner.os}} - name: install ginac - if: matrix.other == 'singletest' + if: matrix.other == '/singletest' run: | pwd cd .. diff --git a/pyomo/contrib/simplification/__init__.py b/pyomo/contrib/simplification/__init__.py index c09e8b8b5e5..3abe5a25ba0 100644 --- a/pyomo/contrib/simplification/__init__.py +++ b/pyomo/contrib/simplification/__init__.py @@ -1 +1 @@ -from .simplify import Simplifier \ No newline at end of file +from .simplify import Simplifier From 546dad1d46cc1a60c6fae711a6dbe8710f53220c Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 16:23:06 -0700 Subject: [PATCH 036/128] install ginac in GHA --- .github/workflows/test_branches.yml | 10 ++++++++-- pyomo/contrib/simplification/simplify.py | 4 ++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index bea57f314e5..16ce6a44003 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -167,14 +167,14 @@ jobs: cd cln-1.3.6 ./configure make - make install + sudo make install cd curl https://www.ginac.de/ginac-1.8.7.tar.bz2 >ginac-1.8.7.tar.bz2 tar -xvf ginac-1.8.7.tar.bz2 cd ginac-1.8.7 ./configure make - make install + sudo make install - name: TPL package download cache uses: actions/cache@v3 @@ -630,6 +630,12 @@ jobs: echo "" pyomo build-extensions --parallel 2 + - name: Install GiNaC Interface + if: matrix.other == '/singletest' + run: | + cd pyomo/contrib/simplification/ + $PYTHON_EXE build.py --inplace + - name: Report pyomo plugin information run: | echo "$PATH" diff --git a/pyomo/contrib/simplification/simplify.py b/pyomo/contrib/simplification/simplify.py index 8f7f15f3826..4002f1a233f 100644 --- a/pyomo/contrib/simplification/simplify.py +++ b/pyomo/contrib/simplification/simplify.py @@ -34,10 +34,10 @@ def simplify_with_ginac(expr: NumericExpression, ginac_interface): class Simplifier(object): - def __init__(self, supress_no_ginac_warnings: bool = False) -> None: + def __init__(self, suppress_no_ginac_warnings: bool = False) -> None: if ginac_available: self.gi = GinacInterface(False) - self.suppress_no_ginac_warnings = supress_no_ginac_warnings + self.suppress_no_ginac_warnings = suppress_no_ginac_warnings def simplify(self, expr: NumericExpression): if ginac_available: From 37a955bdfaccec8508887dbf037eb5ea72b5b5cb Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 16:24:16 -0700 Subject: [PATCH 037/128] install ginac in GHA --- .github/workflows/test_branches.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 16ce6a44003..477361683ac 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -634,7 +634,7 @@ jobs: if: matrix.other == '/singletest' run: | cd pyomo/contrib/simplification/ - $PYTHON_EXE build.py --inplace + $PYTHON_EXE build.py --inplace - name: Report pyomo plugin information run: | From 05134ce49621f1efd16d961dc20e34c7cff23475 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 16:45:55 -0700 Subject: [PATCH 038/128] install ginac in GHA --- .github/workflows/test_branches.yml | 1 + pyomo/contrib/simplification/build.py | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 477361683ac..7933aa522d8 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -633,6 +633,7 @@ jobs: - name: Install GiNaC Interface if: matrix.other == '/singletest' run: | + ls /usr/local/include/ginac/ cd pyomo/contrib/simplification/ $PYTHON_EXE build.py --inplace diff --git a/pyomo/contrib/simplification/build.py b/pyomo/contrib/simplification/build.py index e8bd645756b..39742e1e351 100644 --- a/pyomo/contrib/simplification/build.py +++ b/pyomo/contrib/simplification/build.py @@ -17,6 +17,7 @@ def build_ginac_interface(args=[]): sources.append(os.path.join(dname, fname)) ginac_lib = find_library('ginac') + print(ginac_lib) if ginac_lib is None: raise RuntimeError( 'could not find GiNaC library; please make sure it is in the LD_LIBRARY_PATH environment variable' From 1151927df204f6969704ec7b671e25a1d9083031 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 17:51:34 -0700 Subject: [PATCH 039/128] install ginac in GHA --- .github/workflows/test_branches.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 7933aa522d8..f2bf057da51 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -633,7 +633,7 @@ jobs: - name: Install GiNaC Interface if: matrix.other == '/singletest' run: | - ls /usr/local/include/ginac/ + export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH cd pyomo/contrib/simplification/ $PYTHON_EXE build.py --inplace From 2c4fdbee83ab76b35a863748048ddf0d091f2f3c Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 18:19:05 -0700 Subject: [PATCH 040/128] skip tests when dependencies are not available --- pyomo/contrib/simplification/tests/test_simplification.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pyomo/contrib/simplification/tests/test_simplification.py b/pyomo/contrib/simplification/tests/test_simplification.py index cc278db4d43..c50a906afe7 100644 --- a/pyomo/contrib/simplification/tests/test_simplification.py +++ b/pyomo/contrib/simplification/tests/test_simplification.py @@ -1,11 +1,17 @@ from pyomo.common.unittest import TestCase from pyomo.common import unittest from pyomo.contrib.simplification import Simplifier +from pyomo.contrib.simplification.simplify import ginac_available from pyomo.core.expr.compare import assertExpressionsEqual, compare_expressions import pyomo.environ as pe from pyomo.core.expr.calculus.diff_with_pyomo import reverse_sd +from pyomo.common.dependencies import attempt_import +sympy, sympy_available = attempt_import('sympy') + + +@unittest.skipIf((not sympy_available) and (not ginac_available), 'neither sympy nor ginac are available') @unittest.pytest.mark.simplification class TestSimplification(TestCase): def compare_against_possible_results(self, got, expected_list): From 9ebd79b898aea6827232220f59c615c771686ddb Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 18:26:40 -0700 Subject: [PATCH 041/128] install ginac in GHA --- .github/workflows/test_branches.yml | 2 +- .github/workflows/test_pr_and_main.yml | 16 +++++++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index f2bf057da51..b97b3a682af 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -168,7 +168,7 @@ jobs: ./configure make sudo make install - cd + cd .. curl https://www.ginac.de/ginac-1.8.7.tar.bz2 >ginac-1.8.7.tar.bz2 tar -xvf ginac-1.8.7.tar.bz2 cd ginac-1.8.7 diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index 7345fd45e10..b99d39cf6c7 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -180,23 +180,22 @@ jobs: # key: pkg-${{env.CACHE_VER}}.0-${{runner.os}} - name: install ginac - if: ${{ matrix.other == 'singletest' }} + if: matrix.other == '/singletest' run: | - pwd cd .. curl https://www.ginac.de/CLN/cln-1.3.6.tar.bz2 >cln-1.3.6.tar.bz2 tar -xvf cln-1.3.6.tar.bz2 cd cln-1.3.6 ./configure make - make install - cd + sudo make install + cd .. curl https://www.ginac.de/ginac-1.8.7.tar.bz2 >ginac-1.8.7.tar.bz2 tar -xvf ginac-1.8.7.tar.bz2 cd ginac-1.8.7 ./configure make - make install + sudo make install - name: TPL package download cache uses: actions/cache@v3 @@ -652,6 +651,13 @@ jobs: echo "" pyomo build-extensions --parallel 2 + - name: Install GiNaC Interface + if: matrix.other == '/singletest' + run: | + export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH + cd pyomo/contrib/simplification/ + $PYTHON_EXE build.py --inplace + - name: Report pyomo plugin information run: | echo "$PATH" From 0bd156351b09d93288d618715817fcc4c530114a Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 18:28:39 -0700 Subject: [PATCH 042/128] update simplification tests --- .github/workflows/test_branches.yml | 2 +- .github/workflows/test_pr_and_main.yml | 2 +- pyomo/contrib/simplification/tests/test_simplification.py | 1 - setup.cfg | 1 - 4 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index b97b3a682af..13a5653f9f5 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -97,7 +97,7 @@ jobs: - os: ubuntu-latest python: 3.11 other: /singletest - category: "-m 'neos or importtest or simplification'" + category: "-m 'neos or importtest'" skip_doctest: 1 TARGET: linux PYENV: pip diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index b99d39cf6c7..8a8a9b08030 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -98,7 +98,7 @@ jobs: - os: ubuntu-latest python: 3.11 other: /singletest - category: "-m 'neos or importtest or simplification'" + category: "-m 'neos or importtest'" skip_doctest: 1 TARGET: linux PYENV: pip diff --git a/pyomo/contrib/simplification/tests/test_simplification.py b/pyomo/contrib/simplification/tests/test_simplification.py index c50a906afe7..f3bce9cee54 100644 --- a/pyomo/contrib/simplification/tests/test_simplification.py +++ b/pyomo/contrib/simplification/tests/test_simplification.py @@ -12,7 +12,6 @@ @unittest.skipIf((not sympy_available) and (not ginac_available), 'neither sympy nor ginac are available') -@unittest.pytest.mark.simplification class TestSimplification(TestCase): def compare_against_possible_results(self, got, expected_list): success = False diff --git a/setup.cfg b/setup.cfg index a431e0cd601..b606138f38c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -22,4 +22,3 @@ markers = lp: marks lp tests gams: marks gams tests bar: marks bar tests - simplification: marks simplification tests that have expensive (to install) dependencies From 26007ac42688cbe7554807198ceb20e9d121d68e Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 18:30:57 -0700 Subject: [PATCH 043/128] run black --- pyomo/contrib/simplification/tests/test_simplification.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/simplification/tests/test_simplification.py b/pyomo/contrib/simplification/tests/test_simplification.py index f3bce9cee54..e6b5ae863f6 100644 --- a/pyomo/contrib/simplification/tests/test_simplification.py +++ b/pyomo/contrib/simplification/tests/test_simplification.py @@ -11,7 +11,10 @@ sympy, sympy_available = attempt_import('sympy') -@unittest.skipIf((not sympy_available) and (not ginac_available), 'neither sympy nor ginac are available') +@unittest.skipIf( + (not sympy_available) and (not ginac_available), + 'neither sympy nor ginac are available', +) class TestSimplification(TestCase): def compare_against_possible_results(self, got, expected_list): success = False From fc36411c1c57aaf4720938cf7926cfcf7048bc75 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 18:56:21 -0700 Subject: [PATCH 044/128] add pytest marker for simplification --- .github/workflows/test_branches.yml | 2 +- .github/workflows/test_pr_and_main.yml | 2 +- pyomo/contrib/simplification/tests/test_simplification.py | 1 + setup.cfg | 1 + 4 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 13a5653f9f5..b97b3a682af 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -97,7 +97,7 @@ jobs: - os: ubuntu-latest python: 3.11 other: /singletest - category: "-m 'neos or importtest'" + category: "-m 'neos or importtest or simplification'" skip_doctest: 1 TARGET: linux PYENV: pip diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index 8a8a9b08030..b99d39cf6c7 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -98,7 +98,7 @@ jobs: - os: ubuntu-latest python: 3.11 other: /singletest - category: "-m 'neos or importtest'" + category: "-m 'neos or importtest or simplification'" skip_doctest: 1 TARGET: linux PYENV: pip diff --git a/pyomo/contrib/simplification/tests/test_simplification.py b/pyomo/contrib/simplification/tests/test_simplification.py index e6b5ae863f6..152db93a358 100644 --- a/pyomo/contrib/simplification/tests/test_simplification.py +++ b/pyomo/contrib/simplification/tests/test_simplification.py @@ -15,6 +15,7 @@ (not sympy_available) and (not ginac_available), 'neither sympy nor ginac are available', ) +@unittest.pytest.mark.simplification class TestSimplification(TestCase): def compare_against_possible_results(self, got, expected_list): success = False diff --git a/setup.cfg b/setup.cfg index b606138f38c..855717490b3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -22,3 +22,4 @@ markers = lp: marks lp tests gams: marks gams tests bar: marks bar tests + simplification: tests for expression simplification that have expensive (to install) dependencies From 5ba03e215c51fe2feba6a5cff7b2baa162fcb87f Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 19:30:50 -0700 Subject: [PATCH 045/128] update GHA --- .github/workflows/test_branches.yml | 1 + .github/workflows/test_pr_and_main.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 243f57ea7aa..b73a9cabc81 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -641,6 +641,7 @@ jobs: if: matrix.other == '/singletest' run: | export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH + echo "LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH" >> $GITHUB_ENV cd pyomo/contrib/simplification/ $PYTHON_EXE build.py --inplace diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index 93919ca6bc3..1c36b89710c 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -662,6 +662,7 @@ jobs: if: matrix.other == '/singletest' run: | export LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH + echo "LD_LIBRARY_PATH=/usr/local/lib:$LD_LIBRARY_PATH" >> $GITHUB_ENV cd pyomo/contrib/simplification/ $PYTHON_EXE build.py --inplace From 90cdeba86b2deaa76ce7b62cd9f21f906b057462 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 19:32:33 -0700 Subject: [PATCH 046/128] update GHA --- .github/workflows/test_branches.yml | 4 ++-- .github/workflows/test_pr_and_main.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index b73a9cabc81..e3e0c3e6caf 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -166,14 +166,14 @@ jobs: tar -xvf cln-1.3.6.tar.bz2 cd cln-1.3.6 ./configure - make + make -j 2 sudo make install cd .. curl https://www.ginac.de/ginac-1.8.7.tar.bz2 >ginac-1.8.7.tar.bz2 tar -xvf ginac-1.8.7.tar.bz2 cd ginac-1.8.7 ./configure - make + make -j 2 sudo make install - name: TPL package download cache diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index 1c36b89710c..9edb8b1c65f 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -187,14 +187,14 @@ jobs: tar -xvf cln-1.3.6.tar.bz2 cd cln-1.3.6 ./configure - make + make -j 2 sudo make install cd .. curl https://www.ginac.de/ginac-1.8.7.tar.bz2 >ginac-1.8.7.tar.bz2 tar -xvf ginac-1.8.7.tar.bz2 cd ginac-1.8.7 ./configure - make + make -j 2 sudo make install - name: TPL package download cache From 17cb11d31d72d7ac9ac9f37e1ec306f8d114cec4 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 19:50:50 -0700 Subject: [PATCH 047/128] debugging GHA --- .github/workflows/test_branches.yml | 1 + .github/workflows/test_pr_and_main.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index e3e0c3e6caf..4e3c14cb70e 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -655,6 +655,7 @@ jobs: - name: Run Pyomo tests if: matrix.mpi == 0 run: | + $PYTHON_EXE -c "from pyomo.contrib.simplification.ginac_interface import GinacInterface" $PYTHON_EXE -m pytest -v \ -W ignore::Warning ${{matrix.category}} \ pyomo `pwd`/pyomo-model-libraries \ diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index 9edb8b1c65f..1626964a7e9 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -676,6 +676,7 @@ jobs: - name: Run Pyomo tests if: matrix.mpi == 0 run: | + $PYTHON_EXE -c "from pyomo.contrib.simplification.ginac_interface import GinacInterface" $PYTHON_EXE -m pytest -v \ -W ignore::Warning ${{matrix.category}} \ pyomo `pwd`/pyomo-model-libraries \ From d1fe24400ed424afdfdf65e3f9918faefc98db96 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 20:02:34 -0700 Subject: [PATCH 048/128] test simplification with ginac and sympy --- .../simplification/tests/test_simplification.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/simplification/tests/test_simplification.py b/pyomo/contrib/simplification/tests/test_simplification.py index 152db93a358..096d776460d 100644 --- a/pyomo/contrib/simplification/tests/test_simplification.py +++ b/pyomo/contrib/simplification/tests/test_simplification.py @@ -12,11 +12,10 @@ @unittest.skipIf( - (not sympy_available) and (not ginac_available), - 'neither sympy nor ginac are available', + (not sympy_available) or (ginac_available), + 'sympy is not available', ) -@unittest.pytest.mark.simplification -class TestSimplification(TestCase): +class TestSimplificationSympy(TestCase): def compare_against_possible_results(self, got, expected_list): success = False for exp in expected_list: @@ -111,3 +110,12 @@ def test_unary(self): simp = Simplifier() e2 = simp.simplify(e) assertExpressionsEqual(self, e, e2) + + +@unittest.skipIf( + not ginac_available, + 'GiNaC is not available', +) +@unittest.pytest.mark.simplification +class TestSimplificationGiNaC(TestSimplificationSympy): + pass From 9159c3c5854c7a9d5437f3308321a9fd4a61be6f Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 20:03:18 -0700 Subject: [PATCH 049/128] test simplification with ginac and sympy --- .../tests/test_simplification.py | 38 +++++++++---------- 1 file changed, 18 insertions(+), 20 deletions(-) diff --git a/pyomo/contrib/simplification/tests/test_simplification.py b/pyomo/contrib/simplification/tests/test_simplification.py index 096d776460d..3124d856784 100644 --- a/pyomo/contrib/simplification/tests/test_simplification.py +++ b/pyomo/contrib/simplification/tests/test_simplification.py @@ -35,25 +35,6 @@ def test_simplify(self): expected = x**-1.0 assertExpressionsEqual(self, expected, der2_simp) - def test_param(self): - m = pe.ConcreteModel() - x = m.x = pe.Var() - p = m.p = pe.Param(mutable=True) - e1 = p * x**2 + p * x + p * x**2 - simp = Simplifier() - e2 = simp.simplify(e1) - self.compare_against_possible_results( - e2, - [ - p * x**2.0 * 2.0 + p * x, - p * x + p * x**2.0 * 2.0, - 2.0 * p * x**2.0 + p * x, - p * x + 2.0 * p * x**2.0, - x**2.0 * p * 2.0 + p * x, - p * x + x**2.0 * p * 2.0, - ], - ) - def test_mul(self): m = pe.ConcreteModel() x = m.x = pe.Var() @@ -118,4 +99,21 @@ def test_unary(self): ) @unittest.pytest.mark.simplification class TestSimplificationGiNaC(TestSimplificationSympy): - pass + def test_param(self): + m = pe.ConcreteModel() + x = m.x = pe.Var() + p = m.p = pe.Param(mutable=True) + e1 = p * x**2 + p * x + p * x**2 + simp = Simplifier() + e2 = simp.simplify(e1) + self.compare_against_possible_results( + e2, + [ + p * x**2.0 * 2.0 + p * x, + p * x + p * x**2.0 * 2.0, + 2.0 * p * x**2.0 + p * x, + p * x + 2.0 * p * x**2.0, + x**2.0 * p * 2.0 + p * x, + p * x + x**2.0 * p * 2.0, + ], + ) From 457f2b378b772eae16b47fa789423bca70de43c5 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 22:17:46 -0700 Subject: [PATCH 050/128] fixing simplification tests --- .github/workflows/test_branches.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 4e3c14cb70e..0eb6fe166d9 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -655,7 +655,8 @@ jobs: - name: Run Pyomo tests if: matrix.mpi == 0 run: | - $PYTHON_EXE -c "from pyomo.contrib.simplification.ginac_interface import GinacInterface" + $PYTHON_EXE -c "from pyomo.contrib.simplification.ginac_interface import GinacInterface; print(GinacInterface)" + $PYTHON_EXE -c "from pyomo.contrib.simplification.simplify import ginac_available; print(ginac_available)" $PYTHON_EXE -m pytest -v \ -W ignore::Warning ${{matrix.category}} \ pyomo `pwd`/pyomo-model-libraries \ From 1e7c3f1b2ecf562000088df80f215ddf6d992420 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 22:44:54 -0700 Subject: [PATCH 051/128] fixing simplification tests --- .../simplification/tests/test_simplification.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/pyomo/contrib/simplification/tests/test_simplification.py b/pyomo/contrib/simplification/tests/test_simplification.py index 3124d856784..6badc76b957 100644 --- a/pyomo/contrib/simplification/tests/test_simplification.py +++ b/pyomo/contrib/simplification/tests/test_simplification.py @@ -11,11 +11,7 @@ sympy, sympy_available = attempt_import('sympy') -@unittest.skipIf( - (not sympy_available) or (ginac_available), - 'sympy is not available', -) -class TestSimplificationSympy(TestCase): +class SimplificationMixin: def compare_against_possible_results(self, got, expected_list): success = False for exp in expected_list: @@ -93,12 +89,20 @@ def test_unary(self): assertExpressionsEqual(self, e, e2) +@unittest.skipIf( + (not sympy_available) or (ginac_available), + 'sympy is not available', +) +class TestSimplificationSympy(TestCase, SimplificationMixin): + pass + + @unittest.skipIf( not ginac_available, 'GiNaC is not available', ) @unittest.pytest.mark.simplification -class TestSimplificationGiNaC(TestSimplificationSympy): +class TestSimplificationGiNaC(TestCase, SimplificationMixin): def test_param(self): m = pe.ConcreteModel() x = m.x = pe.Var() From b2d969f73e4dbf1e80d453dd7be4dcd474fc32be Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 22:46:36 -0700 Subject: [PATCH 052/128] fixing simplification tests --- .../simplification/tests/test_simplification.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/pyomo/contrib/simplification/tests/test_simplification.py b/pyomo/contrib/simplification/tests/test_simplification.py index 6badc76b957..e3c60cb02ca 100644 --- a/pyomo/contrib/simplification/tests/test_simplification.py +++ b/pyomo/contrib/simplification/tests/test_simplification.py @@ -89,18 +89,12 @@ def test_unary(self): assertExpressionsEqual(self, e, e2) -@unittest.skipIf( - (not sympy_available) or (ginac_available), - 'sympy is not available', -) +@unittest.skipIf((not sympy_available) or (ginac_available), 'sympy is not available') class TestSimplificationSympy(TestCase, SimplificationMixin): pass -@unittest.skipIf( - not ginac_available, - 'GiNaC is not available', -) +@unittest.skipIf(not ginac_available, 'GiNaC is not available') @unittest.pytest.mark.simplification class TestSimplificationGiNaC(TestCase, SimplificationMixin): def test_param(self): From da2fe3d714b34c7b03a41a2c63af8590db87d2e4 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 10 Jan 2024 23:04:15 -0700 Subject: [PATCH 053/128] fixing simplification tests --- .github/workflows/test_branches.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 0eb6fe166d9..1124a253ac8 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -657,6 +657,7 @@ jobs: run: | $PYTHON_EXE -c "from pyomo.contrib.simplification.ginac_interface import GinacInterface; print(GinacInterface)" $PYTHON_EXE -c "from pyomo.contrib.simplification.simplify import ginac_available; print(ginac_available)" + pytest -v pyomo/contrib/simplification/tests/test_simplification.py $PYTHON_EXE -m pytest -v \ -W ignore::Warning ${{matrix.category}} \ pyomo `pwd`/pyomo-model-libraries \ From e52573202c11e22c632e830fdb2b1c8b492a97cb Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Thu, 11 Jan 2024 17:03:17 -0700 Subject: [PATCH 054/128] Coramin: remove ComponentWeakRef --- pyomo/contrib/coramin/relaxations/alphabb.py | 9 ++---- .../contrib/coramin/relaxations/mccormick.py | 26 +++++------------ .../coramin/relaxations/multivariate.py | 9 ++---- .../coramin/relaxations/relaxations_base.py | 29 +------------------ .../contrib/coramin/relaxations/univariate.py | 18 ++++-------- 5 files changed, 17 insertions(+), 74 deletions(-) diff --git a/pyomo/contrib/coramin/relaxations/alphabb.py b/pyomo/contrib/coramin/relaxations/alphabb.py index 8065e082bea..8fcd273b9b4 100644 --- a/pyomo/contrib/coramin/relaxations/alphabb.py +++ b/pyomo/contrib/coramin/relaxations/alphabb.py @@ -2,7 +2,6 @@ from pyomo.contrib.coramin.relaxations.custom_block import declare_custom_block from pyomo.contrib.coramin.relaxations.relaxations_base import ( BaseRelaxationData, - ComponentWeakRef, ) from pyomo.contrib.coramin.relaxations.hessian import Hessian from typing import Optional, Tuple @@ -19,7 +18,7 @@ class AlphaBBRelaxationData(BaseRelaxationData): def __init__(self, component): super().__init__(component) self._xs: Optional[Tuple[_GeneralVarData]] = None - self._aux_var_ref = ComponentWeakRef(None) + self._aux_var = None self._f_x_expr: Optional[ExpressionBase] = None self._alphabb_rhs: Optional[ExpressionBase] = None self._hessian: Optional[Hessian] = None @@ -32,10 +31,6 @@ def __init__(self, component): def hessian(self): return self._hessian - @property - def _aux_var(self): - return self._aux_var_ref.get_component() - def get_rhs_vars(self) -> Tuple[_GeneralVarData, ...]: return self._xs @@ -89,7 +84,7 @@ def set_input( safety_tol=safety_tol, ) self._xs = tuple(identify_variables(f_x_expr, include_fixed=False)) - self._aux_var_ref.set_component(aux_var) + object.__setattr__(self, '_aux_var', aux_var) self._f_x_expr = f_x_expr if hessian is None: hessian = Hessian( diff --git a/pyomo/contrib/coramin/relaxations/mccormick.py b/pyomo/contrib/coramin/relaxations/mccormick.py index d88396649b9..44a4fc52070 100644 --- a/pyomo/contrib/coramin/relaxations/mccormick.py +++ b/pyomo/contrib/coramin/relaxations/mccormick.py @@ -2,7 +2,7 @@ import pyomo.environ as pyo from pyomo.contrib.coramin.utils.coramin_enums import RelaxationSide from .custom_block import declare_custom_block -from .relaxations_base import BasePWRelaxationData, ComponentWeakRef, _check_cut +from .relaxations_base import BasePWRelaxationData, _check_cut import math from ._utils import check_var_pts, _get_bnds_list, _get_bnds_tuple from pyomo.core.base.param import IndexedParam @@ -136,9 +136,9 @@ class PWMcCormickRelaxationData(BasePWRelaxationData): def __init__(self, component): BasePWRelaxationData.__init__(self, component) - self._x1ref = ComponentWeakRef(None) - self._x2ref = ComponentWeakRef(None) - self._aux_var_ref = ComponentWeakRef(None) + self._x1 = None + self._x2 = None + self._aux_var = None self._f_x_expr = None self._mc_index = None self._slopes_index = None @@ -149,18 +149,6 @@ def __init__(self, component): self._mc_exprs: Dict[int, LinearExpression] = dict() self._pw = None - @property - def _x1(self): - return self._x1ref.get_component() - - @property - def _x2(self): - return self._x2ref.get_component() - - @property - def _aux_var(self): - return self._aux_var_ref.get_component() - def get_rhs_vars(self): return self._x1, self._x2 @@ -218,9 +206,9 @@ def set_input( small_coef=small_coef, safety_tol=safety_tol, ) - self._x1ref.set_component(x1) - self._x2ref.set_component(x2) - self._aux_var_ref.set_component(aux_var) + object.__setattr__(self, '_x1', x1) + object.__setattr__(self, '_x2', x2) + object.__setattr__(self, '_aux_var', aux_var) self._partitions[self._x1] = _get_bnds_list(self._x1) self._f_x_expr = x1 * x2 diff --git a/pyomo/contrib/coramin/relaxations/multivariate.py b/pyomo/contrib/coramin/relaxations/multivariate.py index ac265da751c..e7ee76b25fa 100644 --- a/pyomo/contrib/coramin/relaxations/multivariate.py +++ b/pyomo/contrib/coramin/relaxations/multivariate.py @@ -2,7 +2,6 @@ from pyomo.contrib.coramin.relaxations.custom_block import declare_custom_block from pyomo.contrib.coramin.relaxations.relaxations_base import ( BaseRelaxationData, - ComponentWeakRef, ) from pyomo.core.expr.visitor import identify_variables import math @@ -15,14 +14,10 @@ class MultivariateRelaxationData(BaseRelaxationData): def __init__(self, component): super(MultivariateRelaxationData, self).__init__(component) self._xs = None - self._aux_var_ref = ComponentWeakRef(None) + self._aux_var = None self._f_x_expr = None self._function_shape = FunctionShape.UNKNOWN - @property - def _aux_var(self): - return self._aux_var_ref.get_component() - def get_rhs_vars(self): return self._xs @@ -71,7 +66,7 @@ def set_input( safety_tol=safety_tol, ) self._xs = tuple(identify_variables(f_x_expr, include_fixed=False)) - self._aux_var_ref.set_component(aux_var) + object.__setattr__(self, '_aux_var', aux_var) self._f_x_expr = f_x_expr def build( diff --git a/pyomo/contrib/coramin/relaxations/relaxations_base.py b/pyomo/contrib/coramin/relaxations/relaxations_base.py index 0fbe1258aca..12c6827dc63 100644 --- a/pyomo/contrib/coramin/relaxations/relaxations_base.py +++ b/pyomo/contrib/coramin/relaxations/relaxations_base.py @@ -427,7 +427,7 @@ def _get_pprint_string(self): relational_operator_string = '<=' else: raise ValueError('Unexpected relaxation side') - return f'Relaxation for {self.get_aux_var().name} {relational_operator_string} {str(self.get_rhs_expr())}' + return f'Relaxation for {self.get_aux_var()} {relational_operator_string} {self.get_rhs_expr()}' def pprint(self, ostream=None, verbose=False, prefix=""): if ostream is None: @@ -826,30 +826,3 @@ def get_active_partitions(self): assert upper is not None ans[var] = lower, upper return ans - - -class ComponentWeakRef(object): - """ - This object is used to reference components from a block that are not owned by that block. - """ - - # ToDo: Example in the documentation - def __init__(self, comp): - self.compref = None - self.set_component(comp) - - def get_component(self): - if self.compref is None: - return None - return self.compref() - - def set_component(self, comp): - self.compref = None - if comp is not None: - self.compref = weakref.ref(comp) - - def __setstate__(self, state): - self.set_component(state['compref']) - - def __getstate__(self): - return {'compref': self.get_component()} diff --git a/pyomo/contrib/coramin/relaxations/univariate.py b/pyomo/contrib/coramin/relaxations/univariate.py index 99e151eb31e..a0d0b950051 100644 --- a/pyomo/contrib/coramin/relaxations/univariate.py +++ b/pyomo/contrib/coramin/relaxations/univariate.py @@ -1,6 +1,6 @@ import pyomo.environ as pyo from pyomo.contrib.coramin.utils.coramin_enums import RelaxationSide, FunctionShape -from .relaxations_base import BasePWRelaxationData, ComponentWeakRef, _check_cut +from .relaxations_base import BasePWRelaxationData, _check_cut from .custom_block import declare_custom_block import numpy as np import math @@ -662,8 +662,8 @@ class PWUnivariateRelaxationData(BasePWRelaxationData): def __init__(self, component): super().__init__(component) - self._xref = ComponentWeakRef(None) - self._aux_var_ref = ComponentWeakRef(None) + self._x = None + self._aux_var = None self._pw_repn = 'INC' self._function_shape = FunctionShape.UNKNOWN self._f_x_expr = None @@ -673,14 +673,6 @@ def __init__(self, component): self._secant_intercept: Optional[Union[ScalarParam, IndexedParam]] = None self._pw_secant = None - @property - def _x(self): - return self._xref.get_component() - - @property - def _aux_var(self): - return self._aux_var_ref.get_component() - def get_rhs_vars(self): return (self._x,) @@ -740,8 +732,8 @@ def set_input( self._function_shape = shape self._f_x_expr = f_x_expr - self._xref.set_component(x) - self._aux_var_ref.set_component(aux_var) + object.__setattr__(self, '_x', x) + object.__setattr__(self, '_aux_var', aux_var) bnds_list = _get_bnds_list(self._x) self._partitions[self._x] = bnds_list From b73d0cc67168c65f52fec696b8e9650dc569f178 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Thu, 11 Jan 2024 17:26:13 -0700 Subject: [PATCH 055/128] Coramin: allow aux_var to be a float --- pyomo/contrib/coramin/algorithms/ecp_bounder.py | 4 ++-- pyomo/contrib/coramin/algorithms/multitree/multitree.py | 2 +- pyomo/contrib/coramin/relaxations/relaxations_base.py | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pyomo/contrib/coramin/algorithms/ecp_bounder.py b/pyomo/contrib/coramin/algorithms/ecp_bounder.py index 12e16b6ea03..7c58a647fc5 100644 --- a/pyomo/contrib/coramin/algorithms/ecp_bounder.py +++ b/pyomo/contrib/coramin/algorithms/ecp_bounder.py @@ -192,12 +192,12 @@ def solve(self, model, timer: HierarchicalTimer = None) -> ECPResults: RelaxationSide.BOTH, RelaxationSide.UNDER, }: - viol = pe.value(b.get_rhs_expr()) - b.get_aux_var().value + viol = pe.value(b.get_rhs_expr()) - pe.value(b.get_aux_var()) elif b.is_rhs_concave() and b.relaxation_side in { RelaxationSide.BOTH, RelaxationSide.OVER, }: - viol = b.get_aux_var().value - pe.value(b.get_rhs_expr()) + viol = pe.value(b.get_aux_var()) - pe.value(b.get_rhs_expr()) except (OverflowError, ZeroDivisionError, ValueError) as err: logger.warning("could not generate ECP cut due to " + str(err)) if viol is not None: diff --git a/pyomo/contrib/coramin/algorithms/multitree/multitree.py b/pyomo/contrib/coramin/algorithms/multitree/multitree.py index 0533c8aaac0..52f542aa9d7 100644 --- a/pyomo/contrib/coramin/algorithms/multitree/multitree.py +++ b/pyomo/contrib/coramin/algorithms/multitree/multitree.py @@ -656,7 +656,7 @@ def _partition_helper(self): if err: break - aux_val = b.get_aux_var().value + aux_val = pe.value(b.get_aux_var()) rhs_val = pe.value(b.get_rhs_expr()) if ( aux_val > rhs_val + self.config.feasibility_tolerance diff --git a/pyomo/contrib/coramin/relaxations/relaxations_base.py b/pyomo/contrib/coramin/relaxations/relaxations_base.py index 12c6827dc63..33e15590468 100644 --- a/pyomo/contrib/coramin/relaxations/relaxations_base.py +++ b/pyomo/contrib/coramin/relaxations/relaxations_base.py @@ -365,7 +365,7 @@ def get_deviation(self): ------- float """ - dev = self.get_aux_var().value - pe.value(self.get_rhs_expr()) + dev = pe.value(self.get_aux_var()) - pe.value(self.get_rhs_expr()) if self.relaxation_side is RelaxationSide.BOTH: dev = abs(dev) elif self.relaxation_side is RelaxationSide.UNDER: @@ -632,9 +632,9 @@ def add_cut( rhs_val = None if rhs_val is not None: if self.has_convex_underestimator(): - viol = rhs_val - self.get_aux_var().value + viol = rhs_val - pe.value(self.get_aux_var()) else: - viol = self.get_aux_var().value - rhs_val + viol = pe.value(self.get_aux_var()) - rhs_val if viol > feasibility_tol: needs_cut = True else: From 7c17a189d23d73718ece58095d4e6158367fd5f2 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Fri, 12 Jan 2024 13:33:17 -0700 Subject: [PATCH 056/128] coramin: working on heuristics --- .../binary_multiplication_reformulation.py | 356 ++++++++++++++++++ 1 file changed, 356 insertions(+) create mode 100644 pyomo/contrib/coramin/heuristics/binary_multiplication_reformulation.py diff --git a/pyomo/contrib/coramin/heuristics/binary_multiplication_reformulation.py b/pyomo/contrib/coramin/heuristics/binary_multiplication_reformulation.py new file mode 100644 index 00000000000..0e65fe4091a --- /dev/null +++ b/pyomo/contrib/coramin/heuristics/binary_multiplication_reformulation.py @@ -0,0 +1,356 @@ +import pyomo.environ as pe +import pybnb +from pyomo.core.base.block import _BlockData +from pyomo.core.base.var import _GeneralVarData, ScalarVar +from pyomo.core.base.param import _ParamData, ScalarParam +from pyomo.core.base.expression import _GeneralExpressionData, ScalarExpression +from pyomo.core.expr import numeric_expr +from typing import Tuple, List, Sequence, Optional, MutableMapping +from pyomo.contrib import appsi +from pyomo.contrib.coramin.utils.pyomo_utils import get_objective +import numpy as np +import math +from pyomo.common.collections import ComponentSet, ComponentMap +from pyomo.core.expr.visitor import identify_variables +from mpi4py import MPI +import time +from pyomo.contrib.appsi.fbbt import IntervalTightener, InfeasibleConstraintException +from pyomo.core.expr.visitor import StreamBasedExpressionVisitor +from pyomo.core.expr.numvalue import NumericValue, native_numeric_types +from typing import Union, Sequence +from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr +from pyomo.contrib.coramin.relaxations.mccormick import PWMcCormickRelaxation +from pyomo.contrib.coramin.utils.coramin_enums import RelaxationSide +from pyomo.contrib.coramin.relaxations import iterators +from pyomo.repn.standard_repn import generate_standard_repn + + +class BinaryMultiplicationInfo(object): + def __init__(self, m: _BlockData) -> None: + self.m = m + self.root_node = None + self.constraint_bounds = None + + +def handle_var( + node: Union[NumericValue, float], + args: Sequence[Union[NumericValue, float]], + info: BinaryMultiplicationInfo, +): + if node.is_fixed(): + res = node.value + else: + res = node + return res + + +def handle_float( + node: Union[NumericValue, float], + args: Sequence[Union[NumericValue, float]], + info: BinaryMultiplicationInfo, +): + return node + + +def handle_param( + node: Union[NumericValue, float], + args: Sequence[Union[NumericValue, float]], + info: BinaryMultiplicationInfo, +): + return node.value + + +def handle_sum( + node: Union[NumericValue, float], + args: Sequence[Union[NumericValue, float]], + info: BinaryMultiplicationInfo, +): + return sum(args) + + +def handle_monomial( + node: Union[NumericValue, float], + args: Sequence[Union[NumericValue, float]], + info: BinaryMultiplicationInfo, +): + return args[0]*args[1] + + +def handle_product( + node: Union[NumericValue, float], + args: Sequence[Union[NumericValue, float]], + info: BinaryMultiplicationInfo, +): + x, y = args + xtype = type(x) + ytype = type(y) + + if xtype in native_numeric_types or ytype in native_numeric_types: + return x * y + if ((x.is_variable_type() and x.is_binary()) or (y.is_variable_type() and y.is_binary())): + + def get_new_rel(m): + ndx = len(m.relaxations) + new_rel = PWMcCormickRelaxation() + setattr(m, f'rel{ndx}', new_rel) + m.relaxations.append(new_rel) + return new_rel + + if x.is_variable_type(): + _x = x + else: + _x = info.m.vars.add() + info.m.cons.add(_x == x) + if y.is_variable_type(): + _y = y + else: + _y = info.vars.add() + info.m.cons.add(_y == y) + if info.root_node is node: + clb, cub = info.constraint_bounds + if clb == cub and clb is not None: + rel = get_new_rel(info.m) + rel.build(_x, _y, clb, relaxation_side=RelaxationSide.BOTH, safety_tol=0) + else: + if clb is not None: + rel = get_new_rel(info.m) + rel.build(_x, _y, clb, relaxation_side=RelaxationSide.OVER, safety_tol=0) + if cub is not None: + rel = get_new_rel(info.m) + rel.build(_x, _y, cub, relaxation_side=RelaxationSide.UNDER, safety_tol=0) + return None + else: + z = info.m.vars.add() + zlb, zub = compute_bounds_on_expr(node) + z.setlb(zlb) + z.setub(zub) + rel = get_new_rel(info.m) + rel.build(_x, _y, z, relaxation_side=RelaxationSide.BOTH, safety_tol=0) + return z + return x * y + + +def handle_exp( + node: Union[NumericValue, float], + args: Sequence[Union[NumericValue, float]], + info: BinaryMultiplicationInfo, +): + return pe.exp(args[0]) + + +def handle_log( + node: Union[NumericValue, float], + args: Sequence[Union[NumericValue, float]], + info: BinaryMultiplicationInfo, +): + return pe.log(args[0]) + + +def handle_log10( + node: Union[NumericValue, float], + args: Sequence[Union[NumericValue, float]], + info: BinaryMultiplicationInfo, +): + return pe.log10(args[0]) + + +def handle_sin( + node: Union[NumericValue, float], + args: Sequence[Union[NumericValue, float]], + info: BinaryMultiplicationInfo, +): + return pe.sin(args[0]) + + +def handle_cos( + node: Union[NumericValue, float], + args: Sequence[Union[NumericValue, float]], + info: BinaryMultiplicationInfo, +): + return pe.cos(args[0]) + + +def handle_tan( + node: Union[NumericValue, float], + args: Sequence[Union[NumericValue, float]], + info: BinaryMultiplicationInfo, +): + return pe.tan(args[0]) + + +def handle_asin( + node: Union[NumericValue, float], + args: Sequence[Union[NumericValue, float]], + info: BinaryMultiplicationInfo, +): + return pe.asin(args[0]) + + +def handle_acos( + node: Union[NumericValue, float], + args: Sequence[Union[NumericValue, float]], + info: BinaryMultiplicationInfo, +): + return pe.acos(args[0]) + + +def handle_atan( + node: Union[NumericValue, float], + args: Sequence[Union[NumericValue, float]], + info: BinaryMultiplicationInfo, +): + return pe.atan(args[0]) + + +def handle_sqrt( + node: Union[NumericValue, float], + args: Sequence[Union[NumericValue, float]], + info: BinaryMultiplicationInfo, +): + return pe.sqrt(args[0]) + + +def handle_abs( + node: Union[NumericValue, float], + args: Sequence[Union[NumericValue, float]], + info: BinaryMultiplicationInfo, +): + return pe.abs(args[0]) + + +def handle_div( + node: Union[NumericValue, float], + args: Sequence[Union[NumericValue, float]], + info: BinaryMultiplicationInfo, +): + x, y = args + return x / y + + +def handle_pow( + node: Union[NumericValue, float], + args: Sequence[Union[NumericValue, float]], + info: BinaryMultiplicationInfo, +): + x, y = args + return x ** y + + +def handle_negation( + node: Union[NumericValue, float], + args: Sequence[Union[NumericValue, float]], + info: BinaryMultiplicationInfo, +): + return -args[0] + + +def handle_named_expression( + node: Union[NumericValue, float], + args: Sequence[Union[NumericValue, float]], + info: BinaryMultiplicationInfo, +): + return args[0] + + +unary_handlers = dict() +unary_handlers['exp'] = handle_exp +unary_handlers['log'] = handle_log +unary_handlers['log10'] = handle_log10 +unary_handlers['sin'] = handle_sin +unary_handlers['cos'] = handle_cos +unary_handlers['tan'] = handle_tan +unary_handlers['asin'] = handle_asin +unary_handlers['acos'] = handle_acos +unary_handlers['atan'] = handle_atan +unary_handlers['sqrt'] = handle_sqrt +unary_handlers['abs'] = handle_abs + + +def handle_unary( + node: Union[NumericValue, float], + args: Sequence[Union[NumericValue, float]], + info: BinaryMultiplicationInfo, +): + return unary_handlers[node.getname()](node, args, info) + + +handlers = dict() +handlers[_GeneralVarData] = handle_var +handlers[ScalarVar] = handle_var +handlers[_ParamData] = handle_param +handlers[ScalarParam] = handle_param +handlers[float] = handle_float +handlers[int] = handle_float +handlers[numeric_expr.SumExpression] = handle_sum +handlers[numeric_expr.LinearExpression] = handle_sum +handlers[numeric_expr.MonomialTermExpression] = handle_monomial +handlers[numeric_expr.ProductExpression] = handle_product +handlers[numeric_expr.DivisionExpression] = handle_div +handlers[numeric_expr.PowExpression] = handle_pow +handlers[numeric_expr.NegationExpression] = handle_negation +handlers[numeric_expr.UnaryFunctionExpression] = handle_unary +handlers[numeric_expr.AbsExpression] = handle_abs +handlers[_GeneralExpressionData] = handle_named_expression +handlers[ScalarExpression] = handle_named_expression +handlers[numeric_expr.NPV_SumExpression] = handle_sum +handlers[numeric_expr.NPV_ProductExpression] = handle_product +handlers[numeric_expr.NPV_DivisionExpression] = handle_div +handlers[numeric_expr.NPV_PowExpression] = handle_pow +handlers[numeric_expr.NPV_NegationExpression] = handle_negation +handlers[numeric_expr.NPV_UnaryFunctionExpression] = handle_unary +handlers[numeric_expr.NPV_AbsExpression] = handle_abs + + +class BinaryMultiplicationWalker(StreamBasedExpressionVisitor): + def __init__(self, m: _BlockData): + super().__init__() + self.info = BinaryMultiplicationInfo(m) + + def exitNode(self, node, data): + return handlers[node.__class__](node, data, self.info) + + +def reformulate_binary_multiplication(m: _BlockData): + """ + The goal of this function is to replace f(x) * y = 0 with + a McCormick relaxation when y is binary (in which case the + McCormick relaxation is equivalent). + """ + r = pe.ConcreteModel() + r.vars = pe.VarList() + r.cons = pe.ConstraintList() + r.relaxations = list() + + walker = BinaryMultiplicationWalker(r) + info = walker.info + + for c in iterators.nonrelaxation_component_data_objects( + m, pe.Constraint, active=True, descend_into=True, + ): + repn = generate_standard_repn(c.body, compute_values=True, quadratic=False) + if repn.nonlinear_expr is None: + r.cons.add((c.lb, c.body, c.ub)) + elif not any(v.is_binary() for v in repn.nonlinear_vars): + r.cons.add((c.lb, c.body, c.ub)) + else: + info.root_node = c.body + info.constraint_bounds = (c.lb, c.ub) + new_body = walker.walk_expression(c.body) + if new_body is not None: + r.cons.add((c.lb, new_body, c.ub)) + + for obj in iterators.nonrelaxation_component_data_objects( + m, pe.Objective, active=True, descend_into=True, + ): + repn = generate_standard_repn(obj.expr, compute_values=True, quadratic=False) + if repn.nonlinear_expr is None: + r.obj = pe.Objective(expr=obj.expr, sense=obj.sense) + elif not any(v.is_binary() for v in repn.nonlinear_vars): + r.obj = pe.Objective(expr=obj.expr, sense=obj.sense) + else: + info.root_node = None + info.constraint_bounds = None + new_expr = walker.walk_expression(obj.expr) + r.obj = pe.Objective(expr=new_expr, sense=obj.sense) + + return r From 70237d3f1e8f49f101458337729734b15577fde0 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 15 Jan 2024 09:20:42 -0700 Subject: [PATCH 057/128] coramin: cleanup --- pyomo/contrib/coramin/clone.py | 2 +- .../binary_multiplication_reformulation.py | 12 +----------- pyomo/contrib/coramin/relaxations/mccormick.py | 1 + 3 files changed, 3 insertions(+), 12 deletions(-) diff --git a/pyomo/contrib/coramin/clone.py b/pyomo/contrib/coramin/clone.py index c9a4375631f..fb95e7b0e7b 100644 --- a/pyomo/contrib/coramin/clone.py +++ b/pyomo/contrib/coramin/clone.py @@ -58,7 +58,7 @@ def clone_active_flat(m1: _BlockData, num_clones: int = 1) -> List[_BlockData]: var_map[v] = v aux_var = r.get_aux_var() var_map[aux_var] = aux_var - if not aux_var.is_fixed(): + if not pe.is_fixed(aux_var): all_vars.add(aux_var) new_rel = copy_relaxation_with_local_data(r, var_map) for m2 in clone_list: diff --git a/pyomo/contrib/coramin/heuristics/binary_multiplication_reformulation.py b/pyomo/contrib/coramin/heuristics/binary_multiplication_reformulation.py index 0e65fe4091a..29b659eec39 100644 --- a/pyomo/contrib/coramin/heuristics/binary_multiplication_reformulation.py +++ b/pyomo/contrib/coramin/heuristics/binary_multiplication_reformulation.py @@ -1,20 +1,10 @@ import pyomo.environ as pe -import pybnb from pyomo.core.base.block import _BlockData from pyomo.core.base.var import _GeneralVarData, ScalarVar from pyomo.core.base.param import _ParamData, ScalarParam from pyomo.core.base.expression import _GeneralExpressionData, ScalarExpression from pyomo.core.expr import numeric_expr -from typing import Tuple, List, Sequence, Optional, MutableMapping -from pyomo.contrib import appsi -from pyomo.contrib.coramin.utils.pyomo_utils import get_objective -import numpy as np -import math -from pyomo.common.collections import ComponentSet, ComponentMap -from pyomo.core.expr.visitor import identify_variables -from mpi4py import MPI -import time -from pyomo.contrib.appsi.fbbt import IntervalTightener, InfeasibleConstraintException +from typing import Sequence from pyomo.core.expr.visitor import StreamBasedExpressionVisitor from pyomo.core.expr.numvalue import NumericValue, native_numeric_types from typing import Union, Sequence diff --git a/pyomo/contrib/coramin/relaxations/mccormick.py b/pyomo/contrib/coramin/relaxations/mccormick.py index 44a4fc52070..15b82545569 100644 --- a/pyomo/contrib/coramin/relaxations/mccormick.py +++ b/pyomo/contrib/coramin/relaxations/mccormick.py @@ -250,6 +250,7 @@ def remove_relaxation(self): self._remove_relaxation() def rebuild(self, build_nonlinear_constraint=False, ensure_oa_at_vertices=True): + build_nonlinear_constraint = build_nonlinear_constraint or self._x1.is_fixed() or self._x2.is_fixed() or (self._x1.lb == self._x1.ub and self._x1.lb is not None) or (self._x2.lb == self._x2.ub and self._x2.lb is not None) super(PWMcCormickRelaxationData, self).rebuild( build_nonlinear_constraint=build_nonlinear_constraint, ensure_oa_at_vertices=ensure_oa_at_vertices, From 7bf699ff64b3c14292d214ae75dcb05f2e523a2b Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 17 Jan 2024 16:17:04 -0700 Subject: [PATCH 058/128] working on heuristics --- pyomo/contrib/coramin/heuristics/diving.py | 89 +++++++++++++++++----- 1 file changed, 71 insertions(+), 18 deletions(-) diff --git a/pyomo/contrib/coramin/heuristics/diving.py b/pyomo/contrib/coramin/heuristics/diving.py index 0c9111868d6..63c2ec2590a 100644 --- a/pyomo/contrib/coramin/heuristics/diving.py +++ b/pyomo/contrib/coramin/heuristics/diving.py @@ -9,10 +9,11 @@ import math from pyomo.common.collections import ComponentSet, ComponentMap from pyomo.core.expr.visitor import identify_variables -from mpi4py import MPI -import time - -np.set_printoptions(linewidth=1000) +from pyomo.contrib.appsi.fbbt import IntervalTightener, InfeasibleConstraintException +from typing import Sequence +from .binary_multiplication_reformulation import reformulate_binary_multiplication +from pyomo.contrib.coramin.clone import clone_active_flat +from pyomo.contrib.coramin.relaxations import iterators def collect_vars(m: _BlockData) -> Tuple[List[_GeneralVarData], List[_GeneralVarData], List[_GeneralVarData]]: @@ -53,16 +54,30 @@ def restore_integers(binary_vars: Sequence[_GeneralVarData], integer_vars: Seque class DivingHeuristic(pybnb.Problem): - def __init__(self, m: _BlockData, nlp_solver: appsi.base.Solver) -> None: + def __init__(self, m: _BlockData) -> None: super().__init__() - nlp_solver.config.load_solution = False - binary_vars, integer_vars, all_vars = collect_vars(m) + self.relaxation = clone_active_flat(reformulate_binary_multiplication(m))[0] + + orig_lbs = [v.lb for v in self.relaxation.vars] + orig_ubs = [v.ub for v in self.relaxation.vars] + for r in iterators.relaxation_data_objects(self.relaxation, descend_into=True, active=True): + r.rebuild(build_nonlinear_constraint=True) + tightener = IntervalTightener() + tightener.config.deactivate_satisfied_constraints = False + tightener.perform_fbbt(self.relaxation) + self.tight_lbs = [v.lb for v in self.relaxation.vars] + self.tight_ubs = [v.ub for v in self.relaxation.vars] + for v, lb, ub in zip(self.relaxation.vars, orig_lbs, orig_ubs): + v.setlb(lb) + v.setub(ub) + relax_integers(binary_vars, integer_vars) self.m = m - self.opt = nlp_solver + self.tightener = IntervalTightener() + self.tightener.config.deactivate_satisfied_constraints = False self.all_vars = all_vars self.binary_vars = binary_vars self.integer_vars = integer_vars @@ -82,11 +97,31 @@ def sense(self): return self._sense def bound(self): - res = self.opt.solve(self.m) - if res.best_feasible_objective is None: + orig_lbs = [v.lb for v in self.relaxation.vars] + orig_ubs = [v.ub for v in self.relaxation.vars] + + for v, lb, ub in zip(self.relaxation.vars, self.tight_lbs, self.tight_ubs): + assert lb is None or math.isfinite(lb) + assert ub is None or math.isfinite(ub) + if v.lb is None or (lb is not None and lb > v.lb): + v.setlb(lb) + if v.ub is None or (ub is not None and ub < v.ub): + v.setub(ub) + + for r in iterators.relaxation_data_objects(self.relaxation, descend_into=True, active=True): + r.rebuild() + r.pprint(verbose=True) + + for v, lb, ub in zip(self.relaxation.vars, orig_lbs, orig_ubs): + v.setlb(lb) + v.setub(ub) + + opt = pe.SolverFactory('ipopt') + res = opt.solve(self.relaxation, skip_trivial_constraints=True, load_solutions=False, tee=False) + if not pe.check_optimal_termination(res): return self.infeasible_objective() - res.solution_loader.load_vars([v for v in self.bin_and_int_vars if not v.is_fixed()]) - ret = res.best_feasible_objective + self.relaxation.solutions.load_from(res) + ret = pe.value(self.obj.expr) if self._sense == pybnb.minimize: ret = max(self.current_node.bound, ret) else: @@ -98,12 +133,30 @@ def objective(self): vals = [v.value for v in unfixed_vars] for v in unfixed_vars: v.fix(round(v.value)) - res = self.opt.solve(self.m) - if res.best_feasible_objective is None: + orig_bounds = [v.bounds for v in self.all_vars] + success = True + try: + self.tightener.perform_fbbt(self.m) + except InfeasibleConstraintException: + success = False + for v, (lb, ub) in zip(self.all_vars, orig_bounds): + v.setlb(lb) + v.setub(ub) + if success: + opt = pe.SolverFactory('ipopt') + opt.options['max_iter'] = 300 + try: + res = opt.solve(self.m, skip_trivial_constraints=True, load_solutions=False, tee=False) + except: + success = False + + if not success: + ret = self.infeasible_objective() + elif not pe.check_optimal_termination(res): ret = self.infeasible_objective() else: - ret = res.best_feasible_objective - res.solution_loader.load_vars() + self.m.solutions.load_from(res) + ret = pe.value(self.obj.expr) sol = np.array([v.value for v in self.all_vars], dtype=float) xl, xu, _ = self.current_node.state self.current_node.state = (xl, xu, sol) @@ -181,8 +234,8 @@ def assert_feasible(m: _BlockData, var_list: Sequence[_GeneralVarData], feasibil assert abs(val - round(val)) <= integer_tol -def run_diving_heuristic(m: _BlockData, nlp_solver: appsi.base.Solver, feasibility_tol: float = 1e-6, integer_tol: float = 1e-4, time_limit: float = 300, node_limit: int = 1000): - prob = DivingHeuristic(m, nlp_solver) +def run_diving_heuristic(m: _BlockData, feasibility_tol: float = 1e-6, integer_tol: float = 1e-4, time_limit: float = 300, node_limit: int = 1000): + prob = DivingHeuristic(m) res: pybnb.SolverResults = pybnb.solve(prob, queue_strategy=pybnb.QueueStrategy.bound, objective_stop=prob.infeasible_objective(), node_limit=node_limit, time_limit=time_limit) ss = pybnb.SolutionStatus if res.solution_status in {ss.feasible, ss.optimal}: From ee615eaa2e0f10627edf99e09b49808b88b9f785 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 17 Jan 2024 16:42:14 -0700 Subject: [PATCH 059/128] working on heuristics --- pyomo/contrib/coramin/clone.py | 6 +++--- .../heuristics/binary_multiplication_reformulation.py | 2 ++ pyomo/contrib/coramin/heuristics/diving.py | 1 - 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/coramin/clone.py b/pyomo/contrib/coramin/clone.py index fb95e7b0e7b..f036116d339 100644 --- a/pyomo/contrib/coramin/clone.py +++ b/pyomo/contrib/coramin/clone.py @@ -51,13 +51,13 @@ def clone_active_flat(m1: _BlockData, num_clones: int = 1) -> List[_BlockData]: rel_list.append(r) for ndx, r in enumerate(rel_list): - var_map = ComponentMap() + var_map = dict() for v in r.get_rhs_vars(): if not v.is_fixed(): all_vars.add(v) - var_map[v] = v + var_map[id(v)] = v aux_var = r.get_aux_var() - var_map[aux_var] = aux_var + var_map[id(aux_var)] = aux_var if not pe.is_fixed(aux_var): all_vars.add(aux_var) new_rel = copy_relaxation_with_local_data(r, var_map) diff --git a/pyomo/contrib/coramin/heuristics/binary_multiplication_reformulation.py b/pyomo/contrib/coramin/heuristics/binary_multiplication_reformulation.py index 29b659eec39..55231892c56 100644 --- a/pyomo/contrib/coramin/heuristics/binary_multiplication_reformulation.py +++ b/pyomo/contrib/coramin/heuristics/binary_multiplication_reformulation.py @@ -13,6 +13,7 @@ from pyomo.contrib.coramin.utils.coramin_enums import RelaxationSide from pyomo.contrib.coramin.relaxations import iterators from pyomo.repn.standard_repn import generate_standard_repn +from pyomo.gdp.disjunct import AutoLinkedBinaryVar class BinaryMultiplicationInfo(object): @@ -267,6 +268,7 @@ def handle_unary( handlers = dict() handlers[_GeneralVarData] = handle_var handlers[ScalarVar] = handle_var +handlers[AutoLinkedBinaryVar] = handle_var handlers[_ParamData] = handle_param handlers[ScalarParam] = handle_param handlers[float] = handle_float diff --git a/pyomo/contrib/coramin/heuristics/diving.py b/pyomo/contrib/coramin/heuristics/diving.py index 63c2ec2590a..5c3a6b2d83a 100644 --- a/pyomo/contrib/coramin/heuristics/diving.py +++ b/pyomo/contrib/coramin/heuristics/diving.py @@ -110,7 +110,6 @@ def bound(self): for r in iterators.relaxation_data_objects(self.relaxation, descend_into=True, active=True): r.rebuild() - r.pprint(verbose=True) for v, lb, ub in zip(self.relaxation.vars, orig_lbs, orig_ubs): v.setlb(lb) From 2b1596b7ccda4b3a68747d5c6916b0c7570dae85 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 22 Jan 2024 04:50:02 -0700 Subject: [PATCH 060/128] fixing simplification tests --- .github/workflows/test_branches.yml | 2 +- pyomo/contrib/simplification/simplify.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 89cb12d3eac..9743e45be41 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -655,7 +655,7 @@ jobs: run: | $PYTHON_EXE -c "from pyomo.contrib.simplification.ginac_interface import GinacInterface; print(GinacInterface)" $PYTHON_EXE -c "from pyomo.contrib.simplification.simplify import ginac_available; print(ginac_available)" - pytest -v pyomo/contrib/simplification/tests/test_simplification.py + pytest -v -m 'simplification' pyomo/contrib/simplification/tests/test_simplification.py $PYTHON_EXE -m pytest -v \ -W ignore::Warning ${{matrix.category}} \ pyomo `pwd`/pyomo-model-libraries \ diff --git a/pyomo/contrib/simplification/simplify.py b/pyomo/contrib/simplification/simplify.py index 4002f1a233f..5c0c5b859e7 100644 --- a/pyomo/contrib/simplification/simplify.py +++ b/pyomo/contrib/simplification/simplify.py @@ -6,7 +6,6 @@ try: from pyomo.contrib.simplification.ginac_interface import GinacInterface - ginac_available = True except: GinacInterface = None From d75c5f892a5dce5d6841c11c9dc02494c61e87b5 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 22 Jan 2024 04:55:31 -0700 Subject: [PATCH 061/128] fixing simplification tests --- .github/workflows/test_branches.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 9743e45be41..2384ea58e2a 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -162,9 +162,9 @@ jobs: run: | pwd cd .. - curl https://www.ginac.de/CLN/cln-1.3.6.tar.bz2 >cln-1.3.6.tar.bz2 - tar -xvf cln-1.3.6.tar.bz2 - cd cln-1.3.6 + curl https://www.ginac.de/CLN/cln-1.3.7.tar.bz2 >cln-1.3.7.tar.bz2 + tar -xvf cln-1.3.7.tar.bz2 + cd cln-1.3.7 ./configure make -j 2 sudo make install From dae02054827c2ae23d608e07a2e6913a15180631 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 22 Jan 2024 05:00:16 -0700 Subject: [PATCH 062/128] run black --- pyomo/contrib/simplification/simplify.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyomo/contrib/simplification/simplify.py b/pyomo/contrib/simplification/simplify.py index 5c0c5b859e7..4002f1a233f 100644 --- a/pyomo/contrib/simplification/simplify.py +++ b/pyomo/contrib/simplification/simplify.py @@ -6,6 +6,7 @@ try: from pyomo.contrib.simplification.ginac_interface import GinacInterface + ginac_available = True except: GinacInterface = None From 313108d131f04e80deb59862e95099a731c84006 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 22 Jan 2024 05:15:38 -0700 Subject: [PATCH 063/128] fixing simplification tests --- .github/workflows/test_branches.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 2384ea58e2a..60f971c0c51 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -657,7 +657,7 @@ jobs: $PYTHON_EXE -c "from pyomo.contrib.simplification.simplify import ginac_available; print(ginac_available)" pytest -v -m 'simplification' pyomo/contrib/simplification/tests/test_simplification.py $PYTHON_EXE -m pytest -v \ - -W ignore::Warning ${{matrix.category}} \ + ${{matrix.category}} \ pyomo `pwd`/pyomo-model-libraries \ `pwd`/examples `pwd`/doc --junitxml="TEST-pyomo.xml" From 5ff4d4d3b03370580da4578e141b98d2bdaaadf0 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 22 Jan 2024 05:24:22 -0700 Subject: [PATCH 064/128] fixing simplification tests --- .github/workflows/test_branches.yml | 2 +- setup.cfg | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 60f971c0c51..5ed5f908d28 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -97,7 +97,7 @@ jobs: - os: ubuntu-latest python: 3.11 other: /singletest - category: "-m 'neos or importtest or simplification'" + category: "-m 'simplification'" skip_doctest: 1 TARGET: linux PYENV: pip diff --git a/setup.cfg b/setup.cfg index 855717490b3..e8b6933bbbc 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,7 +5,6 @@ license_files = LICENSE.md universal=1 [tool:pytest] -filterwarnings = ignore::RuntimeWarning junit_family = xunit2 markers = default: mark a test that should always run by default From 010a997ff78390af504d2dc0493cf8e96ee5fd7f Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 22 Jan 2024 05:47:50 -0700 Subject: [PATCH 065/128] fixing simplification tests --- .github/workflows/test_branches.yml | 9 +++------ setup.cfg | 1 + 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 5ed5f908d28..b766583e259 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -97,7 +97,7 @@ jobs: - os: ubuntu-latest python: 3.11 other: /singletest - category: "-m 'simplification'" + category: "-m 'neos or importtest or simplification'" skip_doctest: 1 TARGET: linux PYENV: pip @@ -653,11 +653,8 @@ jobs: - name: Run Pyomo tests if: matrix.mpi == 0 run: | - $PYTHON_EXE -c "from pyomo.contrib.simplification.ginac_interface import GinacInterface; print(GinacInterface)" - $PYTHON_EXE -c "from pyomo.contrib.simplification.simplify import ginac_available; print(ginac_available)" - pytest -v -m 'simplification' pyomo/contrib/simplification/tests/test_simplification.py - $PYTHON_EXE -m pytest -v \ - ${{matrix.category}} \ + pytest -v \ + -W ignore::Warning ${{matrix.category}} \ pyomo `pwd`/pyomo-model-libraries \ `pwd`/examples `pwd`/doc --junitxml="TEST-pyomo.xml" diff --git a/setup.cfg b/setup.cfg index e8b6933bbbc..855717490b3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -5,6 +5,7 @@ license_files = LICENSE.md universal=1 [tool:pytest] +filterwarnings = ignore::RuntimeWarning junit_family = xunit2 markers = default: mark a test that should always run by default From 2c59b2930d2d1b41dde9f919810a3028f248ed1d Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 22 Jan 2024 06:22:27 -0700 Subject: [PATCH 066/128] fixing simplification tests --- .github/workflows/test_branches.yml | 9 +++++++-- pyomo/core/expr/compare.py | 14 +------------- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index b766583e259..15880896961 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -97,7 +97,7 @@ jobs: - os: ubuntu-latest python: 3.11 other: /singletest - category: "-m 'neos or importtest or simplification'" + category: "-m 'neos or importtest'" skip_doctest: 1 TARGET: linux PYENV: pip @@ -653,11 +653,16 @@ jobs: - name: Run Pyomo tests if: matrix.mpi == 0 run: | - pytest -v \ + $PYTHON_EXE -m pytest -v \ -W ignore::Warning ${{matrix.category}} \ pyomo `pwd`/pyomo-model-libraries \ `pwd`/examples `pwd`/doc --junitxml="TEST-pyomo.xml" + - name: Run Simplification Tests + if: matrix.other == '/singletest' + run: | + pytest -v -m 'simplification' pyomo.contrib.simplification/tests/test_simplification.py --junitxml="TEST-pyomo-simplify.xml" + - name: Run Pyomo MPI tests if: matrix.mpi != 0 run: | diff --git a/pyomo/core/expr/compare.py b/pyomo/core/expr/compare.py index 96913f1de39..ec8d56896b8 100644 --- a/pyomo/core/expr/compare.py +++ b/pyomo/core/expr/compare.py @@ -195,19 +195,7 @@ def compare_expressions(expr1, expr2, include_named_exprs=True): expr2, include_named_exprs=include_named_exprs ) try: - res = True - if len(pn1) != len(pn2): - res = False - if res: - for a, b in zip(pn1, pn2): - if a.__class__ is not b.__class__: - res = False - break - if a == b: - continue - else: - res = False - break + res = pn1 == pn2 except PyomoException: res = False return res From d67d90d8c1226d0afa96ff700e173819eb822b7f Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 22 Jan 2024 06:25:16 -0700 Subject: [PATCH 067/128] fixing simplification tests --- .github/workflows/test_branches.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 15880896961..5eac447fd02 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -650,6 +650,11 @@ jobs: pyomo help --transformations || exit 1 pyomo help --writers || exit 1 + - name: Run Simplification Tests + if: matrix.other == '/singletest' + run: | + pytest -v -m 'simplification' pyomo.contrib.simplification/tests/test_simplification.py --junitxml="TEST-pyomo-simplify.xml" + - name: Run Pyomo tests if: matrix.mpi == 0 run: | @@ -658,11 +663,6 @@ jobs: pyomo `pwd`/pyomo-model-libraries \ `pwd`/examples `pwd`/doc --junitxml="TEST-pyomo.xml" - - name: Run Simplification Tests - if: matrix.other == '/singletest' - run: | - pytest -v -m 'simplification' pyomo.contrib.simplification/tests/test_simplification.py --junitxml="TEST-pyomo-simplify.xml" - - name: Run Pyomo MPI tests if: matrix.mpi != 0 run: | From 3fcec359e1f70842af03fe9fdb8b0629785a1b64 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 22 Jan 2024 06:31:28 -0700 Subject: [PATCH 068/128] fixing simplification tests --- .github/workflows/test_branches.yml | 1 - .github/workflows/test_pr_and_main.yml | 14 +++++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 5eac447fd02..d66451a00a5 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -160,7 +160,6 @@ jobs: - name: install ginac if: matrix.other == '/singletest' run: | - pwd cd .. curl https://www.ginac.de/CLN/cln-1.3.7.tar.bz2 >cln-1.3.7.tar.bz2 tar -xvf cln-1.3.7.tar.bz2 diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index 9b82c565c32..bda6014b352 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -98,7 +98,7 @@ jobs: - os: ubuntu-latest python: '3.11' other: /singletest - category: "-m 'neos or importtest or simplification'" + category: "-m 'neos or importtest'" skip_doctest: 1 TARGET: linux PYENV: pip @@ -183,9 +183,9 @@ jobs: if: matrix.other == '/singletest' run: | cd .. - curl https://www.ginac.de/CLN/cln-1.3.6.tar.bz2 >cln-1.3.6.tar.bz2 - tar -xvf cln-1.3.6.tar.bz2 - cd cln-1.3.6 + curl https://www.ginac.de/CLN/cln-1.3.7.tar.bz2 >cln-1.3.7.tar.bz2 + tar -xvf cln-1.3.7.tar.bz2 + cd cln-1.3.7 ./configure make -j 2 sudo make install @@ -671,10 +671,14 @@ jobs: pyomo help --transformations || exit 1 pyomo help --writers || exit 1 + - name: Run Simplification Tests + if: matrix.other == '/singletest' + run: | + pytest -v -m 'simplification' pyomo.contrib.simplification/tests/test_simplification.py --junitxml="TEST-pyomo-simplify.xml" + - name: Run Pyomo tests if: matrix.mpi == 0 run: | - $PYTHON_EXE -c "from pyomo.contrib.simplification.ginac_interface import GinacInterface" $PYTHON_EXE -m pytest -v \ -W ignore::Warning ${{matrix.category}} \ pyomo `pwd`/pyomo-model-libraries \ From b5550fecb393d2ee9eeebba0e3df66b7360a10d9 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 22 Jan 2024 06:38:34 -0700 Subject: [PATCH 069/128] fixing simplification tests --- .github/workflows/test_branches.yml | 2 +- .github/workflows/test_pr_and_main.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index d66451a00a5..83e652cbef8 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -652,7 +652,7 @@ jobs: - name: Run Simplification Tests if: matrix.other == '/singletest' run: | - pytest -v -m 'simplification' pyomo.contrib.simplification/tests/test_simplification.py --junitxml="TEST-pyomo-simplify.xml" + pytest -v -m 'simplification' pyomo/contrib/simplification/tests/test_simplification.py --junitxml="TEST-pyomo-simplify.xml" - name: Run Pyomo tests if: matrix.mpi == 0 diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index bda6014b352..6df28fbadc9 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -674,7 +674,7 @@ jobs: - name: Run Simplification Tests if: matrix.other == '/singletest' run: | - pytest -v -m 'simplification' pyomo.contrib.simplification/tests/test_simplification.py --junitxml="TEST-pyomo-simplify.xml" + pytest -v -m 'simplification' pyomo/contrib/simplification/tests/test_simplification.py --junitxml="TEST-pyomo-simplify.xml" - name: Run Pyomo tests if: matrix.mpi == 0 From 8984389e71d57fe7b9d56c80331ea207166f1290 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 22 Jan 2024 06:52:04 -0700 Subject: [PATCH 070/128] fixing simplification tests --- pyomo/core/expr/compare.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/core/expr/compare.py b/pyomo/core/expr/compare.py index ec8d56896b8..61ff8660a8b 100644 --- a/pyomo/core/expr/compare.py +++ b/pyomo/core/expr/compare.py @@ -196,7 +196,7 @@ def compare_expressions(expr1, expr2, include_named_exprs=True): ) try: res = pn1 == pn2 - except PyomoException: + except (PyomoException, AttributeError): res = False return res From 85fba4221090bb4a5515c0f119b7793dbfde7d11 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 22 Jan 2024 07:31:48 -0700 Subject: [PATCH 071/128] coramin: cleanup --- pyomo/contrib/coramin/domain_reduction/obbt.py | 2 +- pyomo/contrib/coramin/relaxations/auto_relax.py | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/coramin/domain_reduction/obbt.py b/pyomo/contrib/coramin/domain_reduction/obbt.py index 066f5c568a7..0ee07572b36 100644 --- a/pyomo/contrib/coramin/domain_reduction/obbt.py +++ b/pyomo/contrib/coramin/domain_reduction/obbt.py @@ -158,7 +158,7 @@ def _single_solve( else: new_bnd = None msg = f'Warning: Bounds tightening for lb for var {str(v)} was unsuccessful. Termination condition: {results.termination_condition}; The lb was not changed.' - logger.warning(msg) + logger.debug(msg) if lb_or_ub == 'lb': orig_lb = pyo.value(v.lb) diff --git a/pyomo/contrib/coramin/relaxations/auto_relax.py b/pyomo/contrib/coramin/relaxations/auto_relax.py index 85ae2f74d84..fada7fe9055 100644 --- a/pyomo/contrib/coramin/relaxations/auto_relax.py +++ b/pyomo/contrib/coramin/relaxations/auto_relax.py @@ -368,6 +368,10 @@ def _relax_convex_pow( else: arg1 = replace_sub_expression_with_aux_var(arg1, parent_block) _x = arg1 + assert type(arg2) in {int, float} + if round(arg2) != arg2: + if arg1.lb is None or arg1.lb < 0: + arg1.setlb(0) degree_map[_aux_var] = 1 relaxation = PWUnivariateRelaxation() relaxation.set_input( @@ -423,7 +427,7 @@ def _relax_leaf_to_root_PowExpression( degree_map[res] = 0 return res if not is_constant(arg2): - logger.warning( + logger.debug( 'Only constant exponents are supported: ' + str(arg1**arg2) + '\nReplacing ' @@ -556,7 +560,6 @@ def _relax_leaf_to_root_PowExpression( degree_map[res] = 1 return res else: - assert compute_float_bounds_on_expr(arg1)[0] >= 0 return _relax_convex_pow( arg1=arg1, arg2=arg2, @@ -568,7 +571,7 @@ def _relax_leaf_to_root_PowExpression( ) elif degree1 == 0: if not is_constant(arg1): - logger.warning( + logger.debug( 'Found {0} raised to a variable power. However, {0} does not appear to be constant (maybe ' 'it is or depends on a mutable Param?). Replacing {0} with its value.'.format( str(arg1) From 646bbd75143fee646f24cf41ef796a04c113f1ff Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 22 Jan 2024 09:00:15 -0700 Subject: [PATCH 072/128] coramin: B&B --- pyomo/contrib/coramin/algorithms/bnb/bnb.py | 93 ++++++++++++++++----- 1 file changed, 70 insertions(+), 23 deletions(-) diff --git a/pyomo/contrib/coramin/algorithms/bnb/bnb.py b/pyomo/contrib/coramin/algorithms/bnb/bnb.py index 0230a9e84df..c7d2453486e 100644 --- a/pyomo/contrib/coramin/algorithms/bnb/bnb.py +++ b/pyomo/contrib/coramin/algorithms/bnb/bnb.py @@ -3,6 +3,7 @@ from pyomo.core.base.block import _BlockData from pyomo.common.modeling import unique_component_name import pyomo.environ as pe +from pyomo.common.errors import InfeasibleConstraintException from pyomo.core.expr.visitor import identify_variables from pyomo.common.collections import ComponentSet, ComponentMap from pyomo.contrib import appsi @@ -18,7 +19,7 @@ from pyomo.core.base.var import _GeneralVarData from pyomo.contrib.coramin.heuristics.diving import run_diving_heuristic from pyomo.contrib.coramin.domain_reduction.obbt import perform_obbt -from typing import Tuple, List, Sequence +from typing import Tuple, List, Sequence, Optional import math import numpy as np import logging @@ -73,6 +74,8 @@ def __init__(self): self.integer_tol = self.declare("integer_tol", ConfigValue(default=1e-4)) self.node_limit = self.declare("node_limit", ConfigValue(default=1000000000)) self.mip_gap = 1e-3 + self.num_root_obbt_iters = self.declare("num_root_obbt_iters", ConfigValue(default=3)) + self.node_obbt_frequency = self.declare("node_obbt_frequency", ConfigValue(default=2)) def collect_vars(m: _BlockData) -> Tuple[List[_GeneralVarData], List[_GeneralVarData]]: @@ -116,8 +119,8 @@ def impose_structure(m): del m.nonlinear.cons[key] -def _fix_vars_with_close_bounds(m, tol=1e-12): - for v in m.vars: +def _fix_vars_with_close_bounds(varlist, tol=1e-12): + for v in varlist: if v.is_fixed(): v.setlb(v.value) v.setub(v.value) @@ -136,10 +139,9 @@ def __init__(self, model: _BlockData, config: BnBConfig, feasible_objective=None relaxation: _BlockData = relaxation self.config = config self.config.lp_solver.config.load_solution = False - self.config.lp_solver.update_config.treat_fixed_vars_as_params = True - self.config.nlp_solver.config.load_solution = False + self.relaxation_solution = None - obj = get_objective(nlp) + self.obj = obj = get_objective(nlp) if obj.sense == pe.minimize: self._sense = pybnb.minimize else: @@ -158,7 +160,7 @@ def __init__(self, model: _BlockData, config: BnBConfig, feasible_objective=None relaxation.obj_ineq = pe.Constraint(expr=obj.expr >= feasible_objective) it.perform_fbbt(relaxation) del relaxation.obj_ineq - _fix_vars_with_close_bounds(relaxation) + _fix_vars_with_close_bounds(relaxation.vars) impose_structure(relaxation) #find_cut_generators(relaxation) @@ -166,6 +168,8 @@ def __init__(self, model: _BlockData, config: BnBConfig, feasible_objective=None self.relaxation_objects = list() for r in iterators.relaxation_data_objects(relaxation, descend_into=True, active=True): self.relaxation_objects.append(r) + r.rebuild(build_nonlinear_constraint=True) + self.interval_tightener.perform_fbbt(self.relaxation) binary_vars, integer_vars = collect_vars(nlp) relax_integers(binary_vars, integer_vars) @@ -191,20 +195,51 @@ def __init__(self, model: _BlockData, config: BnBConfig, feasible_objective=None self.current_node: Optional[pybnb.Node] = None self.feasible_objective = feasible_objective - for iter in range(3): + if self._sense == pybnb.minimize: + if self.feasible_objective is None: + feasible_objective = math.inf + else: + feasible_objective = self.feasible_objective + feasible_objective += abs(feasible_objective) * 1e-3 + 1e-3 + else: + if self.feasible_objective is None: + feasible_objective = -math.inf + else: + feasible_objective = self.feasible_objective + feasible_objective -= abs(feasible_objective) * 1e-3 + 1e-3 + + for _ in range(self.config.num_root_obbt_iters): + for r in self.relaxation_objects: + r.rebuild() perform_obbt( relaxation, solver=self.config.lp_solver, varlist=list(self.rhs_vars), objective_bound=feasible_objective, + parallel=False, ) for r in self.relaxation_objects: - r.rebuild() + r.rebuild(build_nonlinear_constraint=True) + self.interval_tightener.perform_fbbt(self.relaxation) + for r in self.relaxation_objects: + r.rebuild() def sense(self): return self._sense def bound(self): + # Do FBBT + for r in self.relaxation_objects: + r.rebuild(build_nonlinear_constraint=True) + try: + self.interval_tightener.perform_fbbt(self.relaxation) + except InfeasibleConstraintException: + return self.infeasible_objective() + finally: + for r in self.relaxation_objects: + r.rebuild() + + # solve the relaxation res = self.config.lp_solver.solve(self.relaxation) if res.termination_condition == appsi.base.TerminationCondition.infeasible: return self.infeasible_objective() @@ -212,6 +247,7 @@ def bound(self): raise RuntimeError(f"Cannot handle termination condition {res.termination_condition} when solving relaxation") res.solution_loader.load_vars() + # add OA cuts for convex constraints while True: added_cuts = False for r in self.relaxation_objects: @@ -228,6 +264,9 @@ def bound(self): else: break + # save the variable values to reload later + self.relaxation_solution = res.solution_loader.get_primals() + # if the solution is feasible, we are done is_feasible = True for v in self.bin_and_int_vars: @@ -245,7 +284,7 @@ def bound(self): return res.best_feasible_objective # maybe do OBBT - if self.current_node.tree_depth % 2 == 0 and self.current_node.tree_depth != 0: + if self.current_node.tree_depth % self.config.node_obbt_frequency == 0 and self.current_node.tree_depth != 0: should_obbt = True if self._sense == pybnb.minimize: if self.feasible_objective is None: @@ -271,9 +310,13 @@ def bound(self): solver=self.config.lp_solver, varlist=list(self.rhs_vars), objective_bound=feasible_objective, + parallel=False, ) for r in self.relaxation_objects: r.rebuild() + res = self.config.lp_solver.solve(self.relaxation) + res.solution_loader.load_vars() + self.relaxation_solution = res.solution_loader.get_primals() return res.best_objective_bound @@ -283,29 +326,30 @@ def objective(self): if self.current_node.tree_depth % 10 != 0: return self.infeasible_objective() unfixed_vars = [v for v in self.bin_and_int_vars if not v.is_fixed()] - vals = [v.value for v in unfixed_vars] for v in unfixed_vars: v.fix(round(v.value)) - res = self.config.nlp_solver.solve(self.nlp) - if res.best_feasible_objective is None: + try: + res = self.config.nlp_solver.solve(self.nlp, load_solutions=False, skip_trivial_constraints=True, tee=False) + success = True + except: + success = False + if not success or not pe.check_optimal_termination(res): ret = self.infeasible_objective() else: - ret = res.best_feasible_objective + self.nlp.solutions.load_from(res) + ret = pe.value(self.obj.expr) if self.sense == pybnb.minimize: if ret < self.feasible_objective: self.feasible_objective = ret else: if ret > self.feasible_objective: self.feasible_objective = ret - res.solution_loader.load_vars() orig_vars = ComponentSet(self.nlp.vars) sol = np.array([v.value for v in self.all_vars], dtype=float) xl, xu, _, _ = self.current_node.state self.current_node.state = (xl, xu, sol, ret) - for v, val in zip(unfixed_vars, vals): + for v in unfixed_vars: v.unfix() - # we have to restore the values so that branch() works properly - v.set_value(val, skip_validation=True) return ret def get_state(self): @@ -352,10 +396,9 @@ def load_state(self, node): else: v.setub(None) - if lb == ub: - v.fix(lb) - else: - v.unfix() + v.unfix() + + _fix_vars_with_close_bounds(self.all_vars) for r in self.relaxation_objects: r.rebuild() @@ -363,6 +406,10 @@ def load_state(self, node): def branch(self): xl, xu, _, _ = self.get_state() + # relaod the solution to the relaxation to make sure branching happens correctly + for v, val in self.relaxation_solution.items(): + v.set_value(val, skip_validation=True) + var_to_branch_on = None max_viol = 0 for v in self.bin_and_int_vars: @@ -408,7 +455,7 @@ def branch(self): def solve_with_bnb(model: _BlockData, config: BnBConfig, comm=None): # we don't want to modify the original model model, orig_var_map = _get_clone_and_var_map(model) - diving_obj, diving_sol = run_diving_heuristic(model, config.nlp_solver, config.feasibility_tol, config.integer_tol) + diving_obj, diving_sol = run_diving_heuristic(model, config.feasibility_tol, config.integer_tol) prob = _BnB(model, config, feasible_objective=diving_obj) res: pybnb.SolverResults = pybnb.solve( prob, From c20446618335d66088977098a9ab028ca9fedb06 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 22 Jan 2024 09:05:35 -0700 Subject: [PATCH 073/128] coramin: B&B --- pyomo/contrib/coramin/algorithms/bnb/bnb.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/coramin/algorithms/bnb/bnb.py b/pyomo/contrib/coramin/algorithms/bnb/bnb.py index c7d2453486e..64bee1ed85a 100644 --- a/pyomo/contrib/coramin/algorithms/bnb/bnb.py +++ b/pyomo/contrib/coramin/algorithms/bnb/bnb.py @@ -327,7 +327,13 @@ def objective(self): return self.infeasible_objective() unfixed_vars = [v for v in self.bin_and_int_vars if not v.is_fixed()] for v in unfixed_vars: - v.fix(round(v.value)) + val = round(v.value) + if val < v.lb: + val += 1 + if val > v.ub: + val -= 1 + assert v.lb <= val <= v.ub + v.fix(val) try: res = self.config.nlp_solver.solve(self.nlp, load_solutions=False, skip_trivial_constraints=True, tee=False) success = True From b5a1dcf0ce1d4598f1019017b887913ee0291cfb Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 22 Jan 2024 13:53:12 -0700 Subject: [PATCH 074/128] coramin: B&B --- pyomo/contrib/coramin/algorithms/bnb/bnb.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/pyomo/contrib/coramin/algorithms/bnb/bnb.py b/pyomo/contrib/coramin/algorithms/bnb/bnb.py index 64bee1ed85a..a32fe6bcbfa 100644 --- a/pyomo/contrib/coramin/algorithms/bnb/bnb.py +++ b/pyomo/contrib/coramin/algorithms/bnb/bnb.py @@ -98,7 +98,7 @@ def relax_integers(binary_vars: Sequence[_GeneralVarData], integer_vars: Sequenc def impose_structure(m): - m.aux_vars = pe.VarList() + m.aux_vars_structure = pe.VarList() for key, c in list(m.nonlinear.cons.items()): repn: StandardRepn = generate_standard_repn(c.body, quadratic=False, compute_values=True) @@ -109,7 +109,7 @@ def impose_structure(m): linear_coefs = list(repn.linear_coefs) linear_vars = list(repn.linear_vars) for term in expr_list: - v = m.aux_vars.add() + v = m.aux_vars_structure.add() linear_coefs.append(1) linear_vars.append(v) m.vars.append(v) @@ -151,7 +151,7 @@ def __init__(self, model: _BlockData, config: BnBConfig, feasible_objective=None # we can identify things like x**3 is convex because # x >= 0 self.interval_tightener = it = appsi.fbbt.IntervalTightener() - it.config.deactivate_satisfied_constraints = True + it.config.deactivate_satisfied_constraints = False it.config.feasibility_tol = config.feasibility_tol if feasible_objective is not None: if obj.sense == pe.minimize: @@ -164,6 +164,7 @@ def __init__(self, model: _BlockData, config: BnBConfig, feasible_objective=None impose_structure(relaxation) #find_cut_generators(relaxation) + self._do_not_use = relaxation self.relaxation = relaxation = relax(relaxation) self.relaxation_objects = list() for r in iterators.relaxation_data_objects(relaxation, descend_into=True, active=True): @@ -186,6 +187,7 @@ def __init__(self, model: _BlockData, config: BnBConfig, feasible_objective=None var_set = ComponentSet(self.binary_vars + self.integer_vars + self.rhs_vars) other_vars = ComponentSet(i for i in nlp.vars if i not in var_set) other_vars.update(i for i in relaxation.aux_vars.values() if i not in var_set) + other_vars.update(i for i in self._do_not_use.aux_vars_structure.values() if i not in var_set) self.other_vars = other_vars = list(other_vars) self.all_branching_vars = list(binary_vars) + list(integer_vars) + list(self.rhs_vars) @@ -231,6 +233,10 @@ def bound(self): # Do FBBT for r in self.relaxation_objects: r.rebuild(build_nonlinear_constraint=True) + for v in self.binary_vars: + v.domain = pe.Binary + for v in self.integer_vars: + v.domain = pe.Integers try: self.interval_tightener.perform_fbbt(self.relaxation) except InfeasibleConstraintException: @@ -238,6 +244,8 @@ def bound(self): finally: for r in self.relaxation_objects: r.rebuild() + for v in self.bin_and_int_vars: + v.domain = pe.Reals # solve the relaxation res = self.config.lp_solver.solve(self.relaxation) @@ -265,7 +273,7 @@ def bound(self): break # save the variable values to reload later - self.relaxation_solution = res.solution_loader.get_primals() + self.relaxation_solution = res.solution_loader.get_primals() # if the solution is feasible, we are done is_feasible = True @@ -363,8 +371,8 @@ def get_state(self): xu = list() for v in self.bin_and_int_vars: - xl.append(math.ceil(v.lb)) - xu.append(math.floor(v.ub)) + xl.append(math.ceil(v.lb - self.config.integer_tol)) + xu.append(math.floor(v.ub + self.config.integer_tol)) for v in self.rhs_vars + self.other_vars: lb, ub = v.bounds From f74c399037665005ca05aab37818237a752dcff9 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 22 Jan 2024 14:20:09 -0700 Subject: [PATCH 075/128] coramin: B&B --- pyomo/contrib/coramin/heuristics/diving.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyomo/contrib/coramin/heuristics/diving.py b/pyomo/contrib/coramin/heuristics/diving.py index 5c3a6b2d83a..3b01a8c3fae 100644 --- a/pyomo/contrib/coramin/heuristics/diving.py +++ b/pyomo/contrib/coramin/heuristics/diving.py @@ -192,6 +192,9 @@ def load_state(self, node): v.unfix() def branch(self): + if len(self.bin_and_int_vars) == 0: + return pybnb.Node() + xl, xu, _ = self.get_state() dist_list = [(abs(v.value - round(v.value)), ndx) for ndx, v in enumerate(self.bin_and_int_vars)] dist_list.sort(key=lambda i: i[0], reverse=True) From a7349fc9c3e6c2727be6a9007aca86dda02c45a7 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 22 Jan 2024 15:38:35 -0700 Subject: [PATCH 076/128] coramin: cleanup --- pyomo/contrib/coramin/algorithms/bnb/bnb.py | 19 ++- .../contrib/coramin/relaxations/auto_relax.py | 145 ++++++++---------- 2 files changed, 73 insertions(+), 91 deletions(-) diff --git a/pyomo/contrib/coramin/algorithms/bnb/bnb.py b/pyomo/contrib/coramin/algorithms/bnb/bnb.py index a32fe6bcbfa..71288cd08e9 100644 --- a/pyomo/contrib/coramin/algorithms/bnb/bnb.py +++ b/pyomo/contrib/coramin/algorithms/bnb/bnb.py @@ -9,7 +9,7 @@ from pyomo.contrib import appsi from pyomo.common.config import ConfigDict, ConfigValue, PositiveFloat from pyomo.contrib.coramin.clone import clone_active_flat -from pyomo.contrib.coramin.relaxations.auto_relax import relax +from pyomo.contrib.coramin.relaxations.auto_relax import _relax_cloned_model from pyomo.repn.standard_repn import generate_standard_repn, StandardRepn from pyomo.contrib.coramin.relaxations.split_expr import split_expr from pyomo.core.expr.numeric_expr import LinearExpression @@ -98,7 +98,7 @@ def relax_integers(binary_vars: Sequence[_GeneralVarData], integer_vars: Sequenc def impose_structure(m): - m.aux_vars_structure = pe.VarList() + m.aux_vars = pe.VarList() for key, c in list(m.nonlinear.cons.items()): repn: StandardRepn = generate_standard_repn(c.body, quadratic=False, compute_values=True) @@ -109,7 +109,7 @@ def impose_structure(m): linear_coefs = list(repn.linear_coefs) linear_vars = list(repn.linear_vars) for term in expr_list: - v = m.aux_vars_structure.add() + v = m.aux_vars.add() linear_coefs.append(1) linear_vars.append(v) m.vars.append(v) @@ -136,7 +136,7 @@ def __init__(self, model: _BlockData, config: BnBConfig, feasible_objective=None # remove all parameters, fixed variables, etc. nlp, relaxation = clone_active_flat(model, 2) self.nlp: _BlockData = nlp - relaxation: _BlockData = relaxation + self.relaxation: _BlockData = relaxation self.config = config self.config.lp_solver.config.load_solution = False self.relaxation_solution = None @@ -164,8 +164,7 @@ def __init__(self, model: _BlockData, config: BnBConfig, feasible_objective=None impose_structure(relaxation) #find_cut_generators(relaxation) - self._do_not_use = relaxation - self.relaxation = relaxation = relax(relaxation) + _relax_cloned_model(relaxation) self.relaxation_objects = list() for r in iterators.relaxation_data_objects(relaxation, descend_into=True, active=True): self.relaxation_objects.append(r) @@ -185,9 +184,7 @@ def __init__(self, model: _BlockData, config: BnBConfig, feasible_objective=None self.rhs_vars = list(ComponentSet(self.rhs_vars) - int_var_set) var_set = ComponentSet(self.binary_vars + self.integer_vars + self.rhs_vars) - other_vars = ComponentSet(i for i in nlp.vars if i not in var_set) - other_vars.update(i for i in relaxation.aux_vars.values() if i not in var_set) - other_vars.update(i for i in self._do_not_use.aux_vars_structure.values() if i not in var_set) + other_vars = ComponentSet(i for i in relaxation.vars if i not in var_set) self.other_vars = other_vars = list(other_vars) self.all_branching_vars = list(binary_vars) + list(integer_vars) + list(self.rhs_vars) @@ -289,6 +286,9 @@ def bound(self): is_feasible = False break if is_feasible: + sol = np.array([v.value for v in self.all_vars], dtype=float) + xl, xu, _, _ = self.current_node.state + self.current_node.state = (xl, xu, sol, res.best_feasible_objective) return res.best_feasible_objective # maybe do OBBT @@ -358,7 +358,6 @@ def objective(self): else: if ret > self.feasible_objective: self.feasible_objective = ret - orig_vars = ComponentSet(self.nlp.vars) sol = np.array([v.value for v in self.all_vars], dtype=float) xl, xu, _, _ = self.current_node.state self.current_node.state = (xl, xu, sol, ret) diff --git a/pyomo/contrib/coramin/relaxations/auto_relax.py b/pyomo/contrib/coramin/relaxations/auto_relax.py index fada7fe9055..52b5258da3c 100644 --- a/pyomo/contrib/coramin/relaxations/auto_relax.py +++ b/pyomo/contrib/coramin/relaxations/auto_relax.py @@ -49,6 +49,7 @@ from pyomo.core.base.block import _BlockData from .iterators import relaxation_data_objects from pyomo.contrib.coramin.utils.pyomo_utils import get_objective +from pyomo.contrib.coramin.clone import clone_active_flat logger = logging.getLogger(__name__) @@ -107,7 +108,8 @@ def replace_sub_expression_with_aux_var(arg, parent_block): return arg elif arg.is_expression_type(): _var = parent_block.aux_vars.add() - _con = parent_block.aux_cons.add(_var == arg) + parent_block.vars.append(_var) + _con = parent_block.linear.cons.add(_var == arg) fbbt(_con) return _var else: @@ -116,6 +118,7 @@ def replace_sub_expression_with_aux_var(arg, parent_block): def _get_aux_var(parent_block, expr): _aux_var = parent_block.aux_vars.add() + parent_block.vars.append(_aux_var) lb, ub = compute_bounds_on_expr(expr) _aux_var.setlb(lb) _aux_var.setub(ub) @@ -1433,7 +1436,7 @@ def _relax_expr_with_convexity_check( if is_constant(linking_expr): assert value(linking_expr) == 0 else: - parent_block.aux_cons.add(linking_repn.to_expression() == 0) + parent_block.linear.cons.add(linking_repn.to_expression() == 0) res = res_list[0] relaxation_side_map[orig_expr] = RelaxationSide.BOTH else: @@ -1463,44 +1466,27 @@ def _relax_expr_with_convexity_check( return res -def relax( - model, - descend_into=True, -): +def _relax_cloned_model(m): """ Create a convex relaxation of the model. Parameters ---------- - model: pyomo.core.base.block._BlockData or pyomo.core.base.PyomoModel.ConcreteModel - The model or block to be relaxed - descend_into: type or tuple of type, optional - The types of pyomo components that should be checked for constraints to be relaxed. The - default is (Block, Disjunct). - - Returns - ------- m: pyomo.core.base.block._BlockData or pyomo.core.base.PyomoModel.ConcreteModel - The relaxed model + The model or block to be relaxed """ - m = pe.Block(concrete=True) - m.cons = pe.ConstraintList() if not hasattr(m, 'aux_vars'): m.aux_vars = pe.VarList() m.relaxations = pe.Block() - m.aux_cons = pe.ConstraintList() aux_var_map = dict() degree_map = ComponentMap() counter = RelaxationCounter() - for c in nonrelaxation_component_data_objects( - model, ctype=Constraint, active=True, descend_into=descend_into, - ): + for c in m.nonlinear.cons.values(): repn = generate_standard_repn(c.body, quadratic=False, compute_values=True) - if repn.nonlinear_expr is None: - m.cons.add((c.lb, c.body, c.ub)) - continue + assert len(repn.quadratic_vars) == 0 + assert repn.nonlinear_expr is not None cl, cu = c.lb, c.ub if cl is not None and cu is not None: @@ -1514,8 +1500,6 @@ def relax( 'Encountered a constraint without a lower or an upper bound: ' + str(c) ) - assert len(repn.quadratic_vars) == 0 - assert repn.nonlinear_expr is not None if len(repn.linear_vars) > 0: new_body = numeric_expr.LinearExpression( constant=repn.constant, @@ -1536,67 +1520,66 @@ def relax( counter=counter, degree_map=degree_map, ) - m.cons.add((cl, new_body, cu)) - - obj = get_objective(model) - if obj is not None: - degree = polynomial_degree(obj.expr) - if degree is None or degree > 1: - if obj.sense == pe.minimize: - relaxation_side = RelaxationSide.UNDER - elif obj.sense == pe.maximize: - relaxation_side = RelaxationSide.OVER - else: - raise ValueError( - 'Encountered an objective with an unrecognized sense: ' + str(obj) - ) + m.linear.cons.add((cl, new_body, cu)) - repn = generate_standard_repn(obj.expr, quadratic=False, compute_values=True) - assert len(repn.quadratic_vars) == 0 - assert repn.nonlinear_expr is not None - if len(repn.linear_vars) > 0: - new_body = numeric_expr.LinearExpression( - constant=repn.constant, - linear_coefs=repn.linear_coefs, - linear_vars=repn.linear_vars, - ) - else: - new_body = repn.constant - - relaxation_side_map = ComponentMap() - relaxation_side_map[repn.nonlinear_expr] = relaxation_side + if hasattr(m.nonlinear, 'obj'): + obj = m.nonlinear.obj + if obj.sense == pe.minimize: + relaxation_side = RelaxationSide.UNDER + elif obj.sense == pe.maximize: + relaxation_side = RelaxationSide.OVER + else: + raise ValueError( + 'Encountered an objective with an unrecognized sense: ' + str(obj) + ) - new_body += _relax_expr( - expr=repn.nonlinear_expr, - aux_var_map=aux_var_map, - parent_block=m, - relaxation_side_map=relaxation_side_map, - counter=counter, - degree_map=degree_map, + repn = generate_standard_repn(obj.expr, quadratic=False, compute_values=True) + assert len(repn.quadratic_vars) == 0 + assert repn.nonlinear_expr is not None + if len(repn.linear_vars) > 0: + new_body = numeric_expr.LinearExpression( + constant=repn.constant, + linear_coefs=repn.linear_coefs, + linear_vars=repn.linear_vars, ) - m.obj = pe.Objective(expr=new_body, sense=obj.sense) else: - m.obj = pe.Objective(expr=obj.expr, sense=obj.sense) - - rel_list = list() - for r in relaxation_data_objects(model, descend_into=True, active=True): - rel_list.append(r) - - for r in rel_list: - var_map = ComponentMap() - for v in r.get_rhs_vars(): - if not v.is_fixed(): - all_vars.add(v) - var_map[v] = v - aux_var = r.get_aux_var() - var_map[aux_var] = aux_var - if not aux_var.is_fixed(): - all_vars.add(aux_var) - new_rel = copy_relaxation_with_local_data(r, var_map) - setattr(m, f'rel{counter}', new_rel) - counter.increment() + new_body = repn.constant + + relaxation_side_map = ComponentMap() + relaxation_side_map[repn.nonlinear_expr] = relaxation_side + + new_body += _relax_expr( + expr=repn.nonlinear_expr, + aux_var_map=aux_var_map, + parent_block=m, + relaxation_side_map=relaxation_side_map, + counter=counter, + degree_map=degree_map, + ) + m.linear.obj = pe.Objective(expr=new_body, sense=obj.sense) + + del m.nonlinear for relaxation in relaxation_data_objects(m, descend_into=True, active=True): relaxation.rebuild() + +def relax( + model, +): + """ + Create a convex relaxation of the model. + + Parameters + ---------- + model: pyomo.core.base.block._BlockData or pyomo.core.base.PyomoModel.ConcreteModel + The model or block to be relaxed + + Returns + ------- + m: pyomo.core.base.block._BlockData or pyomo.core.base.PyomoModel.ConcreteModel + The relaxed model + """ + m = clone_active_flat(model)[0] + _relax_cloned_model(m) return m From 7b9053a197bc80832fddc18000c67cd191c91598 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 24 Jan 2024 01:06:40 -0700 Subject: [PATCH 077/128] coramin cut generators --- pyomo/contrib/coramin/algorithms/bnb/bnb.py | 153 +++++++++++++++--- .../coramin/cutting_planes/alpha_bb_cuts.py | 52 ++++-- pyomo/contrib/coramin/cutting_planes/base.py | 3 +- pyomo/contrib/coramin/relaxations/hessian.py | 36 ++++- pyomo/contrib/coramin/utils/pyomo_utils.py | 10 +- .../simplification/ginac_interface.cpp | 46 +++--- .../simplification/ginac_interface.hpp | 16 +- 7 files changed, 246 insertions(+), 70 deletions(-) diff --git a/pyomo/contrib/coramin/algorithms/bnb/bnb.py b/pyomo/contrib/coramin/algorithms/bnb/bnb.py index 71288cd08e9..67092ea7c58 100644 --- a/pyomo/contrib/coramin/algorithms/bnb/bnb.py +++ b/pyomo/contrib/coramin/algorithms/bnb/bnb.py @@ -19,6 +19,9 @@ from pyomo.core.base.var import _GeneralVarData from pyomo.contrib.coramin.heuristics.diving import run_diving_heuristic from pyomo.contrib.coramin.domain_reduction.obbt import perform_obbt +from pyomo.contrib.coramin.cutting_planes.alpha_bb_cuts import AlphaBBCutGenerator +from pyomo.contrib.coramin.cutting_planes.base import CutGenerator +from pyomo.contrib.coramin.utils.coramin_enums import EigenValueBounder from typing import Tuple, List, Sequence, Optional import math import numpy as np @@ -62,6 +65,13 @@ def _get_clone_and_var_map(m1: _BlockData): return m2, var_map +class AlphaBBConfig(ConfigDict): + def __init__(self, description=None, doc=None, implicit=False, implicit_domain=None, visibility=0): + super().__init__(description, doc, implicit, implicit_domain, visibility) + self.max_num_vars: int = self.declare("max_num_vars", ConfigValue(default=4)) + self.method: EigenValueBounder = self.declare("method", ConfigValue(default=EigenValueBounder.GershgorinWithSimplification)) + + class BnBConfig(MIPSolverConfig): def __init__(self): super().__init__(None, None, False, None, 0) @@ -76,6 +86,25 @@ def __init__(self): self.mip_gap = 1e-3 self.num_root_obbt_iters = self.declare("num_root_obbt_iters", ConfigValue(default=3)) self.node_obbt_frequency = self.declare("node_obbt_frequency", ConfigValue(default=2)) + self.alphabb = self.declare("alphabb", AlphaBBConfig()) + + +class NodeState(object): + def __init__( + self, + lbs: np.ndarray, + ubs: np.ndarray, + parent: Optional[pybnb.Node], + sol: Optional[np.ndarray] = None, + obj: Optional[float] = None + ) -> None: + self.lbs: np.ndarray = lbs + self.ubs: np.ndarray = ubs + self.parent: Optional[pybnb.Node] = parent + self.sol: Optional[np.ndarray] = sol + self.obj: Optional[float] = obj + self.valid_cut_indices: List[int] = list() + self.active_cut_indices: List[int] = list() def collect_vars(m: _BlockData) -> Tuple[List[_GeneralVarData], List[_GeneralVarData]]: @@ -113,11 +142,39 @@ def impose_structure(m): linear_coefs.append(1) linear_vars.append(v) m.vars.append(v) - m.nonlinear.cons.add(v == term) + if c.equality or (c.lb == c.ub and c.lb is not None): + m.nonlinear.cons.add(v == term) + elif c.ub is None: + m.nonlinear.cons.add(v <= term) + elif c.lb is None: + m.nonlinear.cons.add(v >= term) + else: + m.nonlinear.cons.add(v == term) new_expr = LinearExpression(constant=repn.constant, linear_coefs=linear_coefs, linear_vars=linear_vars) m.linear.cons.add((c.lb, new_expr, c.ub)) del m.nonlinear.cons[key] + if hasattr(m.nonlinear, 'obj'): + obj = m.nonlinear.obj + repn: StandardRepn = generate_standard_repn(obj.expr, quadratic=False, compute_values=True) + expr_list = split_expr(repn.nonlinear_expr) + if len(expr_list) > 1: + linear_coefs = list(repn.linear_coefs) + linear_vars = list(repn.linear_vars) + for term in expr_list: + v = m.aux_vars.add() + linear_coefs.append(1) + linear_vars.append(v) + m.vars.append(v) + if obj.sense == pe.minimize: + m.nonlinear.cons.add(v >= term) + else: + assert obj.sense == pe.maximize + m.nonlinear.cons.add(v <= term) + new_expr = LinearExpression(constant=repn.constant, linear_coefs=linear_coefs, linear_vars=linear_vars) + m.linear.obj = pe.Objective(expr=new_expr, sense=obj.sense) + del m.nonlinear.obj + def _fix_vars_with_close_bounds(varlist, tol=1e-12): for v in varlist: @@ -131,6 +188,29 @@ def _fix_vars_with_close_bounds(varlist, tol=1e-12): v.fix(0.5 * (lb + ub)) +def find_cut_generators(m: _BlockData, config: AlphaBBConfig) -> List[CutGenerator]: + cut_generators = list() + for c in m.nonlinear.cons.values(): + repn: StandardRepn = generate_standard_repn(c.body, quadratic=False, compute_values=True) + if len(repn.nonlinear_vars) > config.max_num_vars: + continue + + if len(repn.linear_coefs) > 0: + lhs = LinearExpression(constant=repn.constant, linear_coefs=repn.linear_coefs, linear_vars=repn.linear_vars) + else: + lhs = repn.constant + + # alpha bb convention is lhs >= rhs + if c.lb is not None: + cg = AlphaBBCutGenerator(lhs=lhs - c.lb, rhs=-repn.nonlinear_expr, eigenvalue_opt=None, method=config.method) + cut_generators.append(cg) + if c.ub is not None: + cg = AlphaBBCutGenerator(lhs=c.ub - lhs, rhs=repn.nonlinear_expr, eigenvalue_opt=None, method=config.method) + cut_generators.append(cg) + + return cut_generators + + class _BnB(pybnb.Problem): def __init__(self, model: _BlockData, config: BnBConfig, feasible_objective=None): # remove all parameters, fixed variables, etc. @@ -163,8 +243,9 @@ def __init__(self, model: _BlockData, config: BnBConfig, feasible_objective=None _fix_vars_with_close_bounds(relaxation.vars) impose_structure(relaxation) - #find_cut_generators(relaxation) + self.cut_generators: List[CutGenerator] = find_cut_generators(relaxation, self.config.alphabb) _relax_cloned_model(relaxation) + relaxation.cuts = pe.ConstraintList() self.relaxation_objects = list() for r in iterators.relaxation_data_objects(relaxation, descend_into=True, active=True): self.relaxation_objects.append(r) @@ -269,6 +350,26 @@ def bound(self): else: break + # add all other types of cuts + while True: + added_cuts = False + for cg in self.cut_generators: + cut_expr = cg.generate(self.current_node) + if cut_expr is not None: + new_con = self.relaxation.cuts.add(cut_expr) + new_con_index = new_con.index() + self.current_node.state.valid_cut_indices.append(new_con_index) + self.current_node.state.active_cut_indices.append(new_con_index) + if added_cuts: + res = self.config.lp_solver.solve(self.relaxation) + if res.termination_condition == appsi.base.TerminationCondition.infeasible: + return self.infeasible_objective() + if res.termination_condition != appsi.base.TerminationCondition.optimal: + raise RuntimeError(f"Cannot handle termination condition {res.termination_condition} when solving relaxation") + res.solution_loader.load_vars() + else: + break + # save the variable values to reload later self.relaxation_solution = res.solution_loader.get_primals() @@ -287,8 +388,8 @@ def bound(self): break if is_feasible: sol = np.array([v.value for v in self.all_vars], dtype=float) - xl, xu, _, _ = self.current_node.state - self.current_node.state = (xl, xu, sol, res.best_feasible_objective) + self.current_node.state.sol = sol + self.current_node.state.obj = res.best_feasible_objective return res.best_feasible_objective # maybe do OBBT @@ -323,14 +424,16 @@ def bound(self): for r in self.relaxation_objects: r.rebuild() res = self.config.lp_solver.solve(self.relaxation) + if res.termination_condition == appsi.base.TerminationCondition.infeasible: + return self.infeasible_objective() res.solution_loader.load_vars() self.relaxation_solution = res.solution_loader.get_primals() return res.best_objective_bound def objective(self): - if self.current_node.state[2] is not None: - return self.current_node.state[3] + if self.current_node.state.sol is not None: + return self.current_node.state.obj if self.current_node.tree_depth % 10 != 0: return self.infeasible_objective() unfixed_vars = [v for v in self.bin_and_int_vars if not v.is_fixed()] @@ -359,13 +462,13 @@ def objective(self): if ret > self.feasible_objective: self.feasible_objective = ret sol = np.array([v.value for v in self.all_vars], dtype=float) - xl, xu, _, _ = self.current_node.state - self.current_node.state = (xl, xu, sol, ret) + self.current_node.state.sol = sol + self.current_node.state.obj = ret for v in unfixed_vars: v.unfix() return ret - def get_state(self): + def get_state(self) -> NodeState: xl = list() xu = list() @@ -387,14 +490,15 @@ def get_state(self): xl = np.array(xl, dtype=float) xu = np.array(xu, dtype=float) - return xl, xu, None, None + return NodeState(xl, xu, None, None, None) def save_state(self, node): node.state = self.get_state() def load_state(self, node): self.current_node = node - xl, xu, _, _ = node.state + xl = node.state.lbs + xu = node.state.ubs xl = [float(i) for i in xl] xu = [float(i) for i in xu] @@ -416,8 +520,16 @@ def load_state(self, node): for r in self.relaxation_objects: r.rebuild() + for c in self.relaxation.cuts.values(): + c.deactivate() + + for ndx in node.state.active_cut_indices: + self.relaxation.cuts[ndx].activate() + def branch(self): - xl, xu, _, _ = self.get_state() + ns = self.get_state() + xl = ns.lbs + xu = ns.ubs # relaod the solution to the relaxation to make sure branching happens correctly for v, val in self.relaxation_solution.items(): @@ -441,7 +553,7 @@ def branch(self): if var_to_branch_on is None: # the relaxation was feasible # no nodes in this part of the tree need explored - return pybnb.Node() + return [] xl1 = xl.copy() xu1 = xu.copy() @@ -458,14 +570,19 @@ def branch(self): xu1[ndx_to_branch_on] = new_ub xl2[ndx_to_branch_on] = new_lb - child1.state = (xl1, xu1, None, None) - child2.state = (xl2, xu2, None, None) + child1.state = NodeState(xl1, xu1, self.current_node, None, None) + child2.state = NodeState(xl2, xu2, self.current_node, None, None) + + child1.state.valid_cut_indices = list(self.current_node.state.valid_cut_indices) + child2.state.valid_cut_indices = list(self.current_node.state.valid_cut_indices) + child1.state.active_cut_indices = list(self.current_node.state.active_cut_indices) + child2.state.active_cut_indices = list(self.current_node.state.active_cut_indices) yield child1 yield child2 -def solve_with_bnb(model: _BlockData, config: BnBConfig, comm=None): +def solve_with_bnb(model: _BlockData, config: BnBConfig): # we don't want to modify the original model model, orig_var_map = _get_clone_and_var_map(model) diving_obj, diving_sol = run_diving_heuristic(model, config.feasibility_tol, config.integer_tol) @@ -477,7 +594,7 @@ def solve_with_bnb(model: _BlockData, config: BnBConfig, comm=None): absolute_gap=config.abs_gap, relative_gap=config.mip_gap, comparison_tolerance=1e-4, - comm=comm, + comm=None, time_limit=config.time_limit, node_limit=config.node_limit, # log=logger, @@ -499,7 +616,7 @@ def solve_with_bnb(model: _BlockData, config: BnBConfig, comm=None): if diving_obj is not None: ret.solution_loader = SolutionLoader(primals={id(orig_var_map[v]): (orig_var_map[v], val) for v, val in diving_sol.items()}, duals=None, slacks=None, reduced_costs=None) else: - vals = best_node.state[2] + vals = best_node.state.sol primals = dict() orig_vars = ComponentSet(prob.nlp.vars) for v, val in zip(prob.all_vars, vals): diff --git a/pyomo/contrib/coramin/cutting_planes/alpha_bb_cuts.py b/pyomo/contrib/coramin/cutting_planes/alpha_bb_cuts.py index 54bf6999276..6bf9fac024d 100644 --- a/pyomo/contrib/coramin/cutting_planes/alpha_bb_cuts.py +++ b/pyomo/contrib/coramin/cutting_planes/alpha_bb_cuts.py @@ -1,36 +1,62 @@ from pyomo.core.base.block import _BlockData from .base import CutGenerator from pyomo.core.base.var import _GeneralVarData -from pyomo.core.expr.numeric_expr import NumericExpression +from pyomo.core.expr.numeric_expr import NumericExpression, NumericValue from pyomo.core.expr.visitor import identify_variables -from typing import List, Optional +from typing import List, Optional, Union from pyomo.contrib.coramin.relaxations.hessian import Hessian from pyomo.contrib.coramin.utils.coramin_enums import EigenValueBounder from pyomo.contrib.appsi.base import Solver from pyomo.core.expr.visitor import value from pyomo.core.expr.relational_expr import RelationalExpression from pyomo.core.expr.taylor_series import taylor_series_expansion +import pybnb class AlphaBBCutGenerator(CutGenerator): - def __init__(self, lhs: _GeneralVarData, rhs: NumericExpression, eigenvalue_opt: Optional[Solver] = None, method: EigenValueBounder = EigenValueBounder.GershgorinWithSimplification) -> None: + def __init__( + self, + lhs: Union[float, int, NumericValue], + rhs: NumericExpression, + eigenvalue_opt: Optional[Solver] = None, + method: EigenValueBounder = EigenValueBounder.GershgorinWithSimplification, + feasibility_tol: float = 1e-6, + ) -> None: self.lhs = lhs self.rhs = rhs self.xlist: List[_GeneralVarData] = list(identify_variables(rhs, include_fixed=False)) self.hessian = Hessian(expr=rhs, opt=eigenvalue_opt, method=method) + self.feasibility_tol = feasibility_tol + self._proven_convex = dict() - def generate(self, model: _BlockData, solver: Solver | None = None) -> Optional[RelationalExpression]: + def _most_recent_ancestor(self, node: pybnb.Node): + res = None + while res is None: + p = node.state.parent + if p is None: + break + if p in self._proven_convex: + res = p + break + node = p + return res + + def generate(self, node: pybnb.Node) -> Optional[RelationalExpression]: lhs_val = value(self.lhs) - if lhs_val >= value(self.rhs): + if lhs_val + self.feasibility_tol >= value(self.rhs): return None - alpha = max(0, -0.5 * self.hessian.get_minimum_eigenvalue()) - alpha_sum = 0 - for ndx, v in enumerate(self.xlist): - lb, ub = v.bounds - alpha_sum += (v - lb) * (v - ub) - alpha_bb_rhs = self.rhs + alpha * alpha_sum - if lhs_val >= value(alpha_bb_rhs): - return None + mra = self._most_recent_ancestor(node) + if mra is not None and self._proven_convex[mra]: + alpha_bb_rhs = self.rhs + else: + alpha = max(0, -0.5 * self.hessian.get_minimum_eigenvalue()) + alpha_sum = 0 + for ndx, v in enumerate(self.xlist): + lb, ub = v.bounds + alpha_sum += (v - lb) * (v - ub) + alpha_bb_rhs = self.rhs + alpha * alpha_sum + if lhs_val + self.feasibility_tol >= value(alpha_bb_rhs): + return None return self.lhs >= taylor_series_expansion(alpha_bb_rhs) diff --git a/pyomo/contrib/coramin/cutting_planes/base.py b/pyomo/contrib/coramin/cutting_planes/base.py index 3cd5edc0501..bf8c95d6e87 100644 --- a/pyomo/contrib/coramin/cutting_planes/base.py +++ b/pyomo/contrib/coramin/cutting_planes/base.py @@ -2,9 +2,10 @@ from pyomo.core.base.block import _BlockData from pyomo.contrib import appsi from typing import Optional +import pybnb class CutGenerator(ABC): @abstractmethod - def generate(self, model: _BlockData, solver: Optional[appsi.base.Solver] = None): + def generate(self, node: pybnb.Node): pass diff --git a/pyomo/contrib/coramin/relaxations/hessian.py b/pyomo/contrib/coramin/relaxations/hessian.py index 16b1c1a8e98..05ccfa96c24 100644 --- a/pyomo/contrib/coramin/relaxations/hessian.py +++ b/pyomo/contrib/coramin/relaxations/hessian.py @@ -51,6 +51,9 @@ def __init__( ): self.method = EigenValueBounder(method) self.opt = opt + self._constant_hessian = False + self._constant_hessian_min_eig = None + self._constant_hessian_max_eig = None self._expr = expr self._var_list = list(identify_variables(expr=expr, include_fixed=False)) self._ndx_map = pe.ComponentMap( @@ -129,8 +132,25 @@ def formulate_eigenvalue_relaxation(self, sense=pe.minimize): self._eigenvalue_relaxation = relaxation return relaxation + def _compute_eigenvalues_of_constant_hessian(self): + assert self._constant_hessian + nvars = len(self._var_list) + h = np.zeros(shape=(nvars, nvars), dtype=float) + for v1, d1 in self._hessian.items(): + ndx1 = self._ndx_map[v1] + for v2, d2 in d1.items(): + ndx2 = self._ndx_map[v2] + h[ndx1, ndx2] = d2 + eigvals = np.linalg.eigvals(h) + self._constant_hessian_min_eig = np.min(eigvals) + self._constant_hessian_max_eig = np.max(eigvals) + def get_minimum_eigenvalue(self): - if self.method <= EigenValueBounder.GershgorinWithSimplification: + if self._constant_hessian: + if self._constant_hessian_min_eig is None: + self._compute_eigenvalues_of_constant_hessian() + res = self._constant_hessian_min_eig + elif self.method <= EigenValueBounder.GershgorinWithSimplification: res = self.bound_eigenvalues_from_interval_hessian()[0] elif self.method == EigenValueBounder.LinearProgram: m = self.formulate_eigenvalue_relaxation() @@ -141,7 +161,11 @@ def get_minimum_eigenvalue(self): return res def get_maximum_eigenvalue(self): - if self.method <= EigenValueBounder.GershgorinWithSimplification: + if self._constant_hessian: + if self._constant_hessian_max_eig is None: + self._compute_eigenvalues_of_constant_hessian() + res = self._constant_hessian_max_eig + elif self.method <= EigenValueBounder.GershgorinWithSimplification: res = self.bound_eigenvalues_from_interval_hessian()[1] elif self.method == EigenValueBounder.LinearProgram: m = self.formulate_eigenvalue_relaxation(sense=pe.maximize) @@ -201,6 +225,14 @@ def compute_symbolic_hessian(self): res[v1][v2] = _der res[v2][v1] = res[v1][v2] + self._constant_hessian = True + for v1, dd in res.items(): + for v2, d2 in dd.items(): + if is_fixed(d2): + res[v1][v2] = pe.value(d2) + else: + self._constant_hessian = False + return res def compute_interval_hessian(self): diff --git a/pyomo/contrib/coramin/utils/pyomo_utils.py b/pyomo/contrib/coramin/utils/pyomo_utils.py index e65774648f8..7dac54a2df9 100644 --- a/pyomo/contrib/coramin/utils/pyomo_utils.py +++ b/pyomo/contrib/coramin/utils/pyomo_utils.py @@ -1,8 +1,7 @@ import pyomo.environ as pe -from pyomo.core.expr.sympy_tools import sympyify_expression, sympy2pyomo_expression from pyomo.core.expr.numvalue import is_fixed -from pyomo.common.collections import ComponentSet from pyomo.core.expr.visitor import identify_variables +from pyomo.contrib.simplification import Simplifier def get_objective(m): @@ -50,10 +49,11 @@ def active_vars(m, include_fixed=False): yield v +simplifier = Simplifier() + + def simplify_expr(expr): - om, se = sympyify_expression(expr) - se = se.simplify() - new_expr = sympy2pyomo_expression(se, om) + new_expr = simplifier.simplify(expr) if is_fixed(new_expr): new_expr = pe.value(new_expr) return new_expr diff --git a/pyomo/contrib/simplification/ginac_interface.cpp b/pyomo/contrib/simplification/ginac_interface.cpp index 32bea8dadd0..ec9bfb20671 100644 --- a/pyomo/contrib/simplification/ginac_interface.cpp +++ b/pyomo/contrib/simplification/ginac_interface.cpp @@ -10,12 +10,12 @@ ex ginac_expr_from_pyomo_node( py::handle expr, std::unordered_map &leaf_map, std::unordered_map &ginac_pyomo_map, - PyomoExprTypes &expr_types, + PyomoExprTypesSimp &expr_types, bool symbolic_solver_labels ) { ex res; - ExprType tmp_type = - expr_types.expr_type_map[py::type::of(expr)].cast(); + ExprTypeSimp tmp_type = + expr_types.expr_type_map[py::type::of(expr)].cast(); switch (tmp_type) { case py_float: { @@ -92,7 +92,7 @@ ex ginac_expr_from_pyomo_node( res = leaf_map[expr_id]; break; } - case ExprType::power: { + case ExprTypeSimp::power: { py::list pyomo_args = expr.attr("args"); res = pow(ginac_expr_from_pyomo_node(pyomo_args[0], leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels), ginac_expr_from_pyomo_node(pyomo_args[1], leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels)); break; @@ -165,14 +165,14 @@ ex pyomo_expr_to_ginac_expr( py::handle expr, std::unordered_map &leaf_map, std::unordered_map &ginac_pyomo_map, - PyomoExprTypes &expr_types, + PyomoExprTypesSimp &expr_types, bool symbolic_solver_labels ) { ex res = ginac_expr_from_pyomo_node(expr, leaf_map, ginac_pyomo_map, expr_types, symbolic_solver_labels); return res; } -ex pyomo_to_ginac(py::handle expr, PyomoExprTypes &expr_types) { +ex pyomo_to_ginac(py::handle expr, PyomoExprTypesSimp &expr_types) { std::unordered_map leaf_map; std::unordered_map ginac_pyomo_map; ex res = ginac_expr_from_pyomo_node(expr, leaf_map, ginac_pyomo_map, expr_types, true); @@ -193,9 +193,9 @@ class GinacToPyomoVisitor public: std::unordered_map *leaf_map; std::unordered_map node_map; - PyomoExprTypes *expr_types; + PyomoExprTypesSimp *expr_types; - GinacToPyomoVisitor(std::unordered_map *_leaf_map, PyomoExprTypes *_expr_types) : leaf_map(_leaf_map), expr_types(_expr_types) {} + GinacToPyomoVisitor(std::unordered_map *_leaf_map, PyomoExprTypesSimp *_expr_types) : leaf_map(_leaf_map), expr_types(_expr_types) {} ~GinacToPyomoVisitor() = default; void visit(const symbol& e) { @@ -287,7 +287,7 @@ py::object GinacInterface::from_ginac(ex &ge) { PYBIND11_MODULE(ginac_interface, m) { m.def("pyomo_to_ginac", &pyomo_to_ginac); - py::class_(m, "PyomoExprTypes").def(py::init<>()); + py::class_(m, "PyomoExprTypesSimp").def(py::init<>()); py::class_(m, "ginac_expression") .def("expand", [](ex &ge) { return ge.expand(); @@ -302,19 +302,19 @@ PYBIND11_MODULE(ginac_interface, m) { .def(py::init()) .def("to_ginac", &GinacInterface::to_ginac) .def("from_ginac", &GinacInterface::from_ginac); - py::enum_(m, "ExprType") - .value("py_float", ExprType::py_float) - .value("var", ExprType::var) - .value("param", ExprType::param) - .value("product", ExprType::product) - .value("sum", ExprType::sum) - .value("negation", ExprType::negation) - .value("external_func", ExprType::external_func) - .value("power", ExprType::power) - .value("division", ExprType::division) - .value("unary_func", ExprType::unary_func) - .value("linear", ExprType::linear) - .value("named_expr", ExprType::named_expr) - .value("numeric_constant", ExprType::numeric_constant) + py::enum_(m, "ExprTypeSimp") + .value("py_float", ExprTypeSimp::py_float) + .value("var", ExprTypeSimp::var) + .value("param", ExprTypeSimp::param) + .value("product", ExprTypeSimp::product) + .value("sum", ExprTypeSimp::sum) + .value("negation", ExprTypeSimp::negation) + .value("external_func", ExprTypeSimp::external_func) + .value("power", ExprTypeSimp::power) + .value("division", ExprTypeSimp::division) + .value("unary_func", ExprTypeSimp::unary_func) + .value("linear", ExprTypeSimp::linear) + .value("named_expr", ExprTypeSimp::named_expr) + .value("numeric_constant", ExprTypeSimp::numeric_constant) .export_values(); } diff --git a/pyomo/contrib/simplification/ginac_interface.hpp b/pyomo/contrib/simplification/ginac_interface.hpp index bc5b0d7b6fc..ec3663c6cc5 100644 --- a/pyomo/contrib/simplification/ginac_interface.hpp +++ b/pyomo/contrib/simplification/ginac_interface.hpp @@ -27,7 +27,7 @@ namespace py = pybind11; using namespace pybind11::literals; using namespace GiNaC; -enum ExprType { +enum ExprTypeSimp { py_float = 0, var = 1, param = 2, @@ -45,9 +45,9 @@ enum ExprType { unary_abs = 14 }; -class PyomoExprTypes { +class PyomoExprTypesSimp { public: - PyomoExprTypes() { + PyomoExprTypesSimp() { expr_type_map[int_] = py_float; expr_type_map[float_] = py_float; expr_type_map[np_int16] = py_float; @@ -75,8 +75,8 @@ class PyomoExprTypes { expr_type_map[NPV_NegationExpression] = negation; expr_type_map[ExternalFunctionExpression] = external_func; expr_type_map[NPV_ExternalFunctionExpression] = external_func; - expr_type_map[PowExpression] = ExprType::power; - expr_type_map[NPV_PowExpression] = ExprType::power; + expr_type_map[PowExpression] = ExprTypeSimp::power; + expr_type_map[NPV_PowExpression] = ExprTypeSimp::power; expr_type_map[DivisionExpression] = division; expr_type_map[NPV_DivisionExpression] = division; expr_type_map[UnaryFunctionExpression] = unary_func; @@ -91,7 +91,7 @@ class PyomoExprTypes { expr_type_map[AbsExpression] = unary_abs; expr_type_map[NPV_AbsExpression] = unary_abs; } - ~PyomoExprTypes() = default; + ~PyomoExprTypesSimp() = default; py::int_ ione = 1; py::float_ fone = 1.0; py::type int_ = py::type::of(ione); @@ -171,14 +171,14 @@ class PyomoExprTypes { py::dict expr_type_map; }; -ex pyomo_to_ginac(py::handle expr, PyomoExprTypes &expr_types); +ex pyomo_to_ginac(py::handle expr, PyomoExprTypesSimp &expr_types); class GinacInterface { public: std::unordered_map leaf_map; std::unordered_map ginac_pyomo_map; - PyomoExprTypes expr_types; + PyomoExprTypesSimp expr_types; bool symbolic_solver_labels = false; GinacInterface() = default; From 9836e14b632c1b80fcf11d4ed6e3622ad2b06b38 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 24 Jan 2024 01:19:56 -0700 Subject: [PATCH 078/128] coramin cleanup --- .../contrib/coramin/relaxations/auto_relax.py | 251 +----------------- 1 file changed, 1 insertion(+), 250 deletions(-) diff --git a/pyomo/contrib/coramin/relaxations/auto_relax.py b/pyomo/contrib/coramin/relaxations/auto_relax.py index 52b5258da3c..6001cca60b1 100644 --- a/pyomo/contrib/coramin/relaxations/auto_relax.py +++ b/pyomo/contrib/coramin/relaxations/auto_relax.py @@ -1,22 +1,16 @@ import pyomo.environ as pe -from pyomo.common.collections import ComponentMap, ComponentSet +from pyomo.common.collections import ComponentMap import pyomo.core.expr.numeric_expr as numeric_expr from pyomo.core.expr.visitor import ExpressionValueVisitor from pyomo.core.expr.numvalue import ( nonpyomo_leaf_types, - value, NumericValue, is_fixed, - polynomial_degree, is_constant, - native_numeric_types, ) -from pyomo.core.expr.numeric_expr import ExpressionBase from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr, fbbt import math -from pyomo.core.base.constraint import Constraint import logging -from .relaxations_base import BaseRelaxationData from .univariate import ( PWUnivariateRelaxation, PWXSquaredRelaxation, @@ -25,30 +19,13 @@ PWArctanRelaxation, ) from .mccormick import PWMcCormickRelaxation -from .multivariate import MultivariateRelaxation -from .alphabb import AlphaBBRelaxation from pyomo.contrib.coramin.utils.coramin_enums import ( RelaxationSide, FunctionShape, - Effort, - EigenValueBounder, ) -from pyomo.gdp import Disjunct from pyomo.core.base.expression import _GeneralExpressionData, SimpleExpression -from pyomo.contrib.coramin.relaxations.iterators import ( - nonrelaxation_component_data_objects, -) -from pyomo.contrib import appsi from pyomo.repn.standard_repn import generate_standard_repn -from pyomo.contrib.fbbt import interval -from pyomo.core.expr.compare import convert_expression_to_prefix_notation -from .split_expr import split_expr -from pyomo.contrib.coramin.utils.pyomo_utils import simplify_expr, active_vars -from .hessian import Hessian -from typing import MutableMapping, Tuple, Union, Optional -from pyomo.core.base.block import _BlockData from .iterators import relaxation_data_objects -from pyomo.contrib.coramin.utils.pyomo_utils import get_objective from pyomo.contrib.coramin.clone import clone_active_flat @@ -1226,24 +1203,6 @@ def visiting_potential_leaf(self, node): return False, None -def _get_prefix_notation(expr): - pn = convert_expression_to_prefix_notation(expr, include_named_exprs=False) - res = list() - for i in pn: - itype = type(i) - if itype is tuple or itype in nonpyomo_leaf_types: - res.append(i) - elif isinstance(i, NumericValue): - if i.is_fixed(): - res.append(pe.value(i)) - else: - assert i.is_variable_type() - res.append(id(i)) - else: - raise NotImplementedError(f'unexpected entry in prefix notation: {str(i)}') - return tuple(res) - - def _relax_expr( expr, aux_var_map, parent_block, relaxation_side_map, counter, degree_map ): @@ -1258,214 +1217,6 @@ def _relax_expr( return new_expr -def _relax_split_expr( - expr: ExpressionBase, - aux_var_map: MutableMapping[ - Tuple, - Tuple[ - NumericValue, - Union[BaseRelaxationData, Tuple[BaseRelaxationData, BaseRelaxationData]], - ], - ], - parent_block: _BlockData, - relaxation_side_map: MutableMapping[NumericValue, RelaxationSide], - counter: RelaxationCounter, - degree_map: MutableMapping[NumericValue, int], - eigenvalue_bounder: EigenValueBounder, - max_vars_per_alpha_bb: int, - max_eigenvalue_for_alpha_bb: float, - eigenvalue_opt: Optional[appsi.base.Solver], -) -> NumericValue: - relaxation_side = relaxation_side_map[expr] - hessian = Hessian(expr, opt=eigenvalue_opt, method=eigenvalue_bounder) - vlist = hessian.variables() - min_eig = hessian.get_minimum_eigenvalue() - max_eig = hessian.get_maximum_eigenvalue() - is_convex = min_eig >= 0 - is_concave = max_eig <= 0 - - all_vars_bounded = True - for v in vlist: - v_lb, v_ub = v.bounds - if v_lb is None or v_ub is None: - all_vars_bounded = False - break - - if len(vlist) == 1 and (is_convex or is_concave): - pn = _get_prefix_notation(expr) - if pn in aux_var_map: - new_expr, relaxation = aux_var_map[pn] - if relaxation_side != relaxation.relaxation_side: - relaxation.relaxation_side = RelaxationSide.BOTH - else: - new_expr = _get_aux_var(parent_block, expr) - relaxation = PWUnivariateRelaxation() - if is_convex: - shape = FunctionShape.CONVEX - else: - shape = FunctionShape.CONCAVE - relaxation.set_input( - x=vlist[0], - aux_var=new_expr, - relaxation_side=relaxation_side, - f_x_expr=expr, - shape=shape, - ) - aux_var_map[pn] = (new_expr, relaxation) - setattr(parent_block.relaxations, 'rel' + str(counter), relaxation) - counter.increment() - degree_map[new_expr] = 1 - elif (is_convex and relaxation_side == RelaxationSide.UNDER) or ( - is_concave and relaxation_side == RelaxationSide.OVER - ): - pn = _get_prefix_notation(expr) - if pn in aux_var_map: - new_expr, (underestimator, overestimator) = aux_var_map[pn] - else: - new_expr, underestimator, overestimator = None, None, None - if new_expr is None: - new_expr = _get_aux_var(parent_block, expr) - if (is_convex and underestimator is None) or ( - is_concave and overestimator is None - ): - relaxation = MultivariateRelaxation() - if is_convex: - shape = FunctionShape.CONVEX - underestimator = relaxation - else: - shape = FunctionShape.CONCAVE - overestimator = relaxation - relaxation.set_input(aux_var=new_expr, shape=shape, f_x_expr=expr) - aux_var_map[pn] = (new_expr, (underestimator, overestimator)) - setattr(parent_block.relaxations, 'rel' + str(counter), relaxation) - counter.increment() - degree_map[new_expr] = 1 - elif ( - all_vars_bounded - and len(vlist) <= max_vars_per_alpha_bb - and ( - ( - relaxation_side == RelaxationSide.UNDER - and min_eig >= -abs(max_eigenvalue_for_alpha_bb) - ) - or ( - relaxation_side == RelaxationSide.OVER - and max_eig <= abs(max_eigenvalue_for_alpha_bb) - ) - ) - ): - pn = _get_prefix_notation(expr) - if pn in aux_var_map: - new_expr, (underestimator, overestimator) = aux_var_map[pn] - else: - new_expr, underestimator, overestimator = None, None, None - if new_expr is None: - new_expr = _get_aux_var(parent_block, expr) - if (relaxation_side == RelaxationSide.UNDER and underestimator is None) or ( - relaxation_side == RelaxationSide.OVER and overestimator is None - ): - relaxation = AlphaBBRelaxation() - relaxation.set_input( - aux_var=new_expr, - f_x_expr=expr, - relaxation_side=relaxation_side, - hessian=hessian, - ) - if relaxation_side == RelaxationSide.UNDER: - underestimator = relaxation - else: - overestimator = relaxation - aux_var_map[pn] = (new_expr, (underestimator, overestimator)) - setattr(parent_block.relaxations, 'rel' + str(counter), relaxation) - counter.increment() - degree_map[new_expr] = 1 - else: - visitor = _FactorableRelaxationVisitor( - aux_var_map=aux_var_map, - parent_block=parent_block, - relaxation_side_map=relaxation_side_map, - counter=counter, - degree_map=degree_map, - ) - new_expr = visitor.dfs_postorder_stack(expr) - return new_expr - - -def _relax_expr_with_convexity_check( - orig_expr: ExpressionBase, - aux_var_map: MutableMapping[ - Tuple, - Tuple[ - NumericValue, - Union[BaseRelaxationData, Tuple[BaseRelaxationData, BaseRelaxationData]], - ], - ], - parent_block: _BlockData, - relaxation_side_map: MutableMapping[NumericValue, RelaxationSide], - counter: RelaxationCounter, - degree_map: MutableMapping[NumericValue, int], - perform_expression_simplification: bool, - eigenvalue_bounder: EigenValueBounder, - max_vars_per_alpha_bb: int, - max_eigenvalue_for_alpha_bb: float, - eigenvalue_opt: Optional[appsi.base.Solver], -): - if relaxation_side_map[orig_expr] == RelaxationSide.BOTH: - res_list = [] - for side in [RelaxationSide.UNDER, RelaxationSide.OVER]: - relaxation_side_map[orig_expr] = side - tmp_res = _relax_expr_with_convexity_check( - orig_expr=orig_expr, - aux_var_map=aux_var_map, - parent_block=parent_block, - relaxation_side_map=relaxation_side_map, - counter=counter, - degree_map=degree_map, - perform_expression_simplification=perform_expression_simplification, - eigenvalue_bounder=eigenvalue_bounder, - max_vars_per_alpha_bb=max_vars_per_alpha_bb, - max_eigenvalue_for_alpha_bb=max_eigenvalue_for_alpha_bb, - eigenvalue_opt=eigenvalue_opt, - ) - res_list.append(tmp_res) - linking_expr = res_list[0] - res_list[1] - linking_repn = generate_standard_repn( - linking_expr, compute_values=False, quadratic=True - ) - linking_expr = linking_repn.to_expression() - if is_constant(linking_expr): - assert value(linking_expr) == 0 - else: - parent_block.linear.cons.add(linking_repn.to_expression() == 0) - res = res_list[0] - relaxation_side_map[orig_expr] = RelaxationSide.BOTH - else: - if perform_expression_simplification: - _expr = simplify_expr(orig_expr) - else: - _expr = orig_expr - list_of_exprs = split_expr(_expr) - list_of_new_exprs = list() - - for expr in list_of_exprs: - relaxation_side_map[expr] = relaxation_side_map[orig_expr] - new_expr = _relax_split_expr( - expr=expr, - aux_var_map=aux_var_map, - parent_block=parent_block, - relaxation_side_map=relaxation_side_map, - counter=counter, - degree_map=degree_map, - eigenvalue_bounder=eigenvalue_bounder, - max_vars_per_alpha_bb=max_vars_per_alpha_bb, - max_eigenvalue_for_alpha_bb=max_eigenvalue_for_alpha_bb, - eigenvalue_opt=eigenvalue_opt, - ) - list_of_new_exprs.append(new_expr) - res = sum(list_of_new_exprs) - return res - - def _relax_cloned_model(m): """ Create a convex relaxation of the model. From 1f28b9fd47db38b0df9c1527d20fc71cc855fafb Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 24 Jan 2024 01:59:13 -0700 Subject: [PATCH 079/128] coramin cleanup --- pyomo/contrib/coramin/algorithms/bnb/bnb.py | 7 +++++-- pyomo/contrib/coramin/heuristics/diving.py | 5 ++++- pyomo/contrib/coramin/relaxations/auto_relax.py | 3 ++- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/coramin/algorithms/bnb/bnb.py b/pyomo/contrib/coramin/algorithms/bnb/bnb.py index 67092ea7c58..73d06bb3873 100644 --- a/pyomo/contrib/coramin/algorithms/bnb/bnb.py +++ b/pyomo/contrib/coramin/algorithms/bnb/bnb.py @@ -76,7 +76,7 @@ class BnBConfig(MIPSolverConfig): def __init__(self): super().__init__(None, None, False, None, 0) self.feasibility_tol = self.declare( - "feasibility_tol", ConfigValue(domain=PositiveFloat, default=1e-8) + "feasibility_tol", ConfigValue(domain=PositiveFloat, default=1e-6) ) self.lp_solver = self.declare("lp_solver", ConfigValue()) self.nlp_solver = self.declare("nlp_solver", ConfigValue()) @@ -192,6 +192,8 @@ def find_cut_generators(m: _BlockData, config: AlphaBBConfig) -> List[CutGenerat cut_generators = list() for c in m.nonlinear.cons.values(): repn: StandardRepn = generate_standard_repn(c.body, quadratic=False, compute_values=True) + if repn.nonlinear_expr is None: + continue if len(repn.nonlinear_vars) > config.max_num_vars: continue @@ -239,7 +241,8 @@ def __init__(self, model: _BlockData, config: BnBConfig, feasible_objective=None else: relaxation.obj_ineq = pe.Constraint(expr=obj.expr >= feasible_objective) it.perform_fbbt(relaxation) - del relaxation.obj_ineq + if feasible_objective is not None: + del relaxation.obj_ineq _fix_vars_with_close_bounds(relaxation.vars) impose_structure(relaxation) diff --git a/pyomo/contrib/coramin/heuristics/diving.py b/pyomo/contrib/coramin/heuristics/diving.py index 3b01a8c3fae..6109ce5ed99 100644 --- a/pyomo/contrib/coramin/heuristics/diving.py +++ b/pyomo/contrib/coramin/heuristics/diving.py @@ -116,7 +116,10 @@ def bound(self): v.setub(ub) opt = pe.SolverFactory('ipopt') - res = opt.solve(self.relaxation, skip_trivial_constraints=True, load_solutions=False, tee=False) + try: + res = opt.solve(self.relaxation, skip_trivial_constraints=True, load_solutions=False, tee=False) + except: + return self.infeasible_objective() if not pe.check_optimal_termination(res): return self.infeasible_objective() self.relaxation.solutions.load_from(res) diff --git a/pyomo/contrib/coramin/relaxations/auto_relax.py b/pyomo/contrib/coramin/relaxations/auto_relax.py index 6001cca60b1..53510a2272b 100644 --- a/pyomo/contrib/coramin/relaxations/auto_relax.py +++ b/pyomo/contrib/coramin/relaxations/auto_relax.py @@ -1237,7 +1237,8 @@ def _relax_cloned_model(m): for c in m.nonlinear.cons.values(): repn = generate_standard_repn(c.body, quadratic=False, compute_values=True) assert len(repn.quadratic_vars) == 0 - assert repn.nonlinear_expr is not None + if repn.nonlinear_expr is None: + continue cl, cu = c.lb, c.ub if cl is not None and cu is not None: From 4b700bab44bbb7562c6979aaaac3c9e2413581f9 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 24 Jan 2024 03:03:59 -0700 Subject: [PATCH 080/128] coramin: update tests --- .../multitree/tests/test_multitree.py | 10 +- .../coramin/relaxations/relaxations_base.py | 4 +- .../relaxations/tests/test_auto_relax.py | 702 ++++++++---------- 3 files changed, 336 insertions(+), 380 deletions(-) diff --git a/pyomo/contrib/coramin/algorithms/multitree/tests/test_multitree.py b/pyomo/contrib/coramin/algorithms/multitree/tests/test_multitree.py index 97954565f95..03540b0bb94 100644 --- a/pyomo/contrib/coramin/algorithms/multitree/tests/test_multitree.py +++ b/pyomo/contrib/coramin/algorithms/multitree/tests/test_multitree.py @@ -3,17 +3,16 @@ from pyomo.contrib import coramin from pyomo.contrib.coramin.third_party.minlplib_tools import ( get_minlplib, - get_minlplib_instancedata, ) from pyomo.common import unittest from pyomo.contrib import appsi import os import logging -from suspect.pyomo.osil_reader import read_osil import math from pyomo.common import download import pyomo.environ as pe from pyomo.core.base.block import _BlockData +import importlib def _get_sol(pname): @@ -68,7 +67,7 @@ def setUpClass(self) -> None: self.primal_sol['autocorr_bern20-03'] = _get_sol('autocorr_bern20-03') self.primal_sol['chem'] = _get_sol('chem') for pname in self.test_problems.keys(): - get_minlplib(problem_name=pname) + get_minlplib(problem_name=pname, format='py') mip_solver = appsi.solvers.Gurobi() nlp_solver = appsi.solvers.Ipopt() nlp_solver.config.log_level = logging.DEBUG @@ -88,8 +87,9 @@ def tearDownClass(self) -> None: def get_model(self, pname): current_dir = os.getcwd() - fname = os.path.join(current_dir, 'minlplib', 'osil', f'{pname}.osil') - m = read_osil(fname, objective_prefix='obj_') + fname = os.path.join(current_dir, 'minlplib', 'py', f'{pname}') + fname = fname.replace('/', '.') + m = importlib.import_module(fname).m return m def _check_primal_sol(self, pname, m: _BlockData, res: appsi.base.Results): diff --git a/pyomo/contrib/coramin/relaxations/relaxations_base.py b/pyomo/contrib/coramin/relaxations/relaxations_base.py index 33e15590468..55abe4ab872 100644 --- a/pyomo/contrib/coramin/relaxations/relaxations_base.py +++ b/pyomo/contrib/coramin/relaxations/relaxations_base.py @@ -111,7 +111,7 @@ def _check_cut( res = (True, None, None, None) for coef_p, v in zip(cut.linear_coefs, cut.linear_vars): coef = coef_p.value - if not math.isfinite(coef) or abs(coef) >= too_large: + if type(coef) is complex or not math.isfinite(coef) or abs(coef) >= too_large: res = (False, v, coef, None) elif 0 < abs(coef) <= too_small and v.has_lb() and v.has_ub(): coef_p._value = 0 @@ -133,7 +133,7 @@ def _check_cut( cut.constant._value -= safety_tol else: cut.constant._value += safety_tol - if not math.isfinite(cut.constant.value) or abs(cut.constant.value) >= too_large: + if type(cut.constant.value) is complex or not math.isfinite(cut.constant.value) or abs(cut.constant.value) >= too_large: res = (False, None, cut.constant.value, None) return res diff --git a/pyomo/contrib/coramin/relaxations/tests/test_auto_relax.py b/pyomo/contrib/coramin/relaxations/tests/test_auto_relax.py index 49bf9f28ec1..da48b6a296b 100644 --- a/pyomo/contrib/coramin/relaxations/tests/test_auto_relax.py +++ b/pyomo/contrib/coramin/relaxations/tests/test_auto_relax.py @@ -10,6 +10,7 @@ from pyomo.core.expr.sympy_tools import sympyify_expression from pyomo.contrib import appsi from pyomo.contrib.coramin.utils import RelaxationSide, Effort, EigenValueBounder +from pyomo.core.expr.compare import assertExpressionsEqual class TestAutoRelax(unittest.TestCase): @@ -20,28 +21,27 @@ def test_product1(self): m.z = pe.Var() m.c = pe.Constraint(expr=m.z - m.x * m.y == 0) - rel = coramin.relaxations.relax(m, use_alpha_bb=False) + rel = coramin.relaxations.relax(m) - self.assertTrue(hasattr(rel, 'aux_cons')) self.assertTrue(hasattr(rel, 'aux_vars')) - self.assertEqual(len(rel.aux_cons), 1) + self.assertEqual(len(rel.linear.cons), 1) self.assertEqual(len(rel.aux_vars), 1) self.assertAlmostEqual(rel.aux_vars[1].lb, -1) self.assertAlmostEqual(rel.aux_vars[1].ub, 1) - self.assertEqual(rel.aux_cons[1].lower, 0) - self.assertEqual(rel.aux_cons[1].upper, 0) - ders = reverse_sd(rel.aux_cons[1].body) - self.assertEqual(ders[rel.z], 1) + self.assertEqual(rel.linear.cons[1].lower, 0) + self.assertEqual(rel.linear.cons[1].upper, 0) + ders = reverse_sd(rel.linear.cons[1].body) + self.assertEqual(ders[m.z], 1) self.assertEqual(ders[rel.aux_vars[1]], -1) - self.assertEqual(len(list(identify_variables(rel.aux_cons[1].body))), 2) + self.assertEqual(len(list(identify_variables(rel.linear.cons[1].body))), 2) self.assertTrue(hasattr(rel, 'relaxations')) self.assertTrue(hasattr(rel.relaxations, 'rel0')) self.assertTrue( isinstance(rel.relaxations.rel0, coramin.relaxations.PWMcCormickRelaxation) ) - self.assertIn(rel.x, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) - self.assertIn(rel.y, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) + self.assertIn(m.x, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) + self.assertIn(m.y, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) self.assertEqual(id(rel.aux_vars[1]), id(rel.relaxations.rel0.get_aux_var())) def test_product2(self): @@ -53,36 +53,35 @@ def test_product2(self): m.c1 = pe.Constraint(expr=m.z - m.x * m.y == 0) m.c2 = pe.Constraint(expr=m.v - 3 * m.x * m.y == 0) - rel = coramin.relaxations.relax(m, use_alpha_bb=False) + rel = coramin.relaxations.relax(m) - self.assertTrue(hasattr(rel, 'aux_cons')) self.assertTrue(hasattr(rel, 'aux_vars')) - self.assertEqual(len(rel.aux_cons), 2) + self.assertEqual(len(rel.linear.cons), 2) self.assertEqual(len(rel.aux_vars), 1) self.assertAlmostEqual(rel.aux_vars[1].lb, -1) self.assertAlmostEqual(rel.aux_vars[1].ub, 1) - self.assertEqual(rel.aux_cons[1].lower, 0) - self.assertEqual(rel.aux_cons[1].upper, 0) - ders = reverse_sd(rel.aux_cons[1].body) - self.assertEqual(ders[rel.z], 1) + self.assertEqual(rel.linear.cons[1].lower, 0) + self.assertEqual(rel.linear.cons[1].upper, 0) + ders = reverse_sd(rel.linear.cons[1].body) + self.assertEqual(ders[m.z], 1) self.assertEqual(ders[rel.aux_vars[1]], -1) - self.assertEqual(len(list(identify_variables(rel.aux_cons[1].body))), 2) + self.assertEqual(len(list(identify_variables(rel.linear.cons[1].body))), 2) - self.assertEqual(rel.aux_cons[2].lower, 0) - self.assertEqual(rel.aux_cons[2].upper, 0) - ders = reverse_sd(rel.aux_cons[2].body) - self.assertEqual(ders[rel.v], 1) + self.assertEqual(rel.linear.cons[2].lower, 0) + self.assertEqual(rel.linear.cons[2].upper, 0) + ders = reverse_sd(rel.linear.cons[2].body) + self.assertEqual(ders[m.v], 1) self.assertEqual(ders[rel.aux_vars[1]], -3) - self.assertEqual(len(list(identify_variables(rel.aux_cons[2].body))), 2) + self.assertEqual(len(list(identify_variables(rel.linear.cons[2].body))), 2) self.assertTrue(hasattr(rel, 'relaxations')) self.assertTrue(hasattr(rel.relaxations, 'rel0')) self.assertTrue( isinstance(rel.relaxations.rel0, coramin.relaxations.PWMcCormickRelaxation) ) - self.assertIn(rel.x, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) - self.assertIn(rel.y, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) + self.assertIn(m.x, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) + self.assertIn(m.y, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) self.assertEqual(id(rel.aux_vars[1]), id(rel.relaxations.rel0.get_aux_var())) def test_product3(self): @@ -92,28 +91,27 @@ def test_product3(self): m.z = pe.Var() m.c = pe.Constraint(expr=m.z - m.x * m.y * 3 == 0) - rel = coramin.relaxations.relax(m, use_alpha_bb=False) + rel = coramin.relaxations.relax(m) - self.assertTrue(hasattr(rel, 'aux_cons')) self.assertTrue(hasattr(rel, 'aux_vars')) - self.assertEqual(len(rel.aux_cons), 1) + self.assertEqual(len(rel.linear.cons), 1) self.assertEqual(len(rel.aux_vars), 1) self.assertAlmostEqual(rel.aux_vars[1].lb, -1) self.assertAlmostEqual(rel.aux_vars[1].ub, 1) - self.assertEqual(rel.aux_cons[1].lower, 0) - self.assertEqual(rel.aux_cons[1].upper, 0) - ders = reverse_sd(rel.aux_cons[1].body) - self.assertEqual(ders[rel.z], 1) + self.assertEqual(rel.linear.cons[1].lower, 0) + self.assertEqual(rel.linear.cons[1].upper, 0) + ders = reverse_sd(rel.linear.cons[1].body) + self.assertEqual(ders[m.z], 1) self.assertEqual(ders[rel.aux_vars[1]], -3) - self.assertEqual(len(list(identify_variables(rel.aux_cons[1].body))), 2) + self.assertEqual(len(list(identify_variables(rel.linear.cons[1].body))), 2) self.assertTrue(hasattr(rel, 'relaxations')) self.assertTrue(hasattr(rel.relaxations, 'rel0')) self.assertTrue( isinstance(rel.relaxations.rel0, coramin.relaxations.PWMcCormickRelaxation) ) - self.assertIn(rel.x, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) - self.assertIn(rel.y, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) + self.assertIn(m.x, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) + self.assertIn(m.y, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) self.assertEqual(id(rel.aux_vars[1]), id(rel.relaxations.rel0.get_aux_var())) relaxations = list(coramin.relaxations.relaxation_data_objects(rel)) self.assertEqual(len(relaxations), 1) @@ -124,20 +122,19 @@ def test_product4(self): m.z = pe.Var() m.c = pe.Constraint(expr=m.z - m.x * m.x == 0) - rel = coramin.relaxations.relax(m, use_alpha_bb=False) + rel = coramin.relaxations.relax(m) - self.assertTrue(hasattr(rel, 'aux_cons')) self.assertTrue(hasattr(rel, 'aux_vars')) - self.assertEqual(len(rel.aux_cons), 1) + self.assertEqual(len(rel.linear.cons), 1) self.assertEqual(len(rel.aux_vars), 1) self.assertAlmostEqual(rel.aux_vars[1].lb, 0) self.assertAlmostEqual(rel.aux_vars[1].ub, 1) - self.assertEqual(rel.aux_cons[1].lower, 0) - self.assertEqual(rel.aux_cons[1].upper, 0) - ders = reverse_sd(rel.aux_cons[1].body) - self.assertEqual(ders[rel.z], 1) + self.assertEqual(rel.linear.cons[1].lower, 0) + self.assertEqual(rel.linear.cons[1].upper, 0) + ders = reverse_sd(rel.linear.cons[1].body) + self.assertEqual(ders[m.z], 1) self.assertEqual(ders[rel.aux_vars[1]], -1) - self.assertEqual(len(list(identify_variables(rel.aux_cons[1].body))), 2) + self.assertEqual(len(list(identify_variables(rel.linear.cons[1].body))), 2) self.assertTrue(hasattr(rel, 'relaxations')) self.assertTrue(hasattr(rel.relaxations, 'rel0')) @@ -146,7 +143,7 @@ def test_product4(self): rel.relaxations.rel0, coramin.relaxations.PWXSquaredRelaxationData ) ) - self.assertIn(rel.x, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) + self.assertIn(m.x, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) self.assertEqual(len(rel.relaxations.rel0.get_rhs_vars()), 1) self.assertEqual(id(rel.aux_vars[1]), id(rel.relaxations.rel0.get_aux_var())) relaxations = list(coramin.relaxations.relaxation_data_objects(rel)) @@ -161,36 +158,35 @@ def test_quadratic(self): m.c = pe.Constraint(expr=m.x**2 + m.y + m.z == 0) m.c2 = pe.Constraint(expr=m.w - 3 * m.x**2 == 0) - rel = coramin.relaxations.relax(m, use_alpha_bb=False) + rel = coramin.relaxations.relax(m) - self.assertTrue(hasattr(rel, 'aux_cons')) self.assertTrue(hasattr(rel, 'aux_vars')) - self.assertEqual(len(rel.aux_cons), 2) + self.assertEqual(len(rel.linear.cons), 2) self.assertEqual(len(rel.aux_vars), 1) self.assertAlmostEqual(rel.aux_vars[1].lb, 0) self.assertAlmostEqual(rel.aux_vars[1].ub, 1) - self.assertEqual(rel.aux_cons[1].lower, 0) - self.assertEqual(rel.aux_cons[1].upper, 0) - ders = reverse_sd(rel.aux_cons[1].body) - self.assertEqual(ders[rel.z], 1) + self.assertEqual(rel.linear.cons[1].lower, 0) + self.assertEqual(rel.linear.cons[1].upper, 0) + ders = reverse_sd(rel.linear.cons[1].body) + self.assertEqual(ders[m.z], 1) self.assertEqual(ders[rel.aux_vars[1]], 1) - self.assertEqual(ders[rel.y], 1) - self.assertEqual(len(list(identify_variables(rel.aux_cons[1].body))), 3) + self.assertEqual(ders[m.y], 1) + self.assertEqual(len(list(identify_variables(rel.linear.cons[1].body))), 3) - self.assertEqual(rel.aux_cons[2].lower, 0) - self.assertEqual(rel.aux_cons[2].upper, 0) - ders = reverse_sd(rel.aux_cons[2].body) - self.assertEqual(ders[rel.w], 1) + self.assertEqual(rel.linear.cons[2].lower, 0) + self.assertEqual(rel.linear.cons[2].upper, 0) + ders = reverse_sd(rel.linear.cons[2].body) + self.assertEqual(ders[m.w], 1) self.assertEqual(ders[rel.aux_vars[1]], -3) - self.assertEqual(len(list(identify_variables(rel.aux_cons[2].body))), 2) + self.assertEqual(len(list(identify_variables(rel.linear.cons[2].body))), 2) self.assertTrue(hasattr(rel, 'relaxations')) self.assertTrue(hasattr(rel.relaxations, 'rel0')) self.assertTrue( isinstance(rel.relaxations.rel0, coramin.relaxations.PWXSquaredRelaxation) ) - self.assertIn(rel.x, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) + self.assertIn(m.x, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) self.assertEqual(id(rel.aux_vars[1]), id(rel.relaxations.rel0.get_aux_var())) self.assertFalse(hasattr(rel.relaxations, 'rel1')) @@ -203,36 +199,35 @@ def test_cubic_convex(self): m.c = pe.Constraint(expr=m.x**3 + m.y + m.z == 0) m.c2 = pe.Constraint(expr=m.w - 3 * m.x**3 == 0) - rel = coramin.relaxations.relax(m, use_alpha_bb=False) + rel = coramin.relaxations.relax(m) - self.assertTrue(hasattr(rel, 'aux_cons')) self.assertTrue(hasattr(rel, 'aux_vars')) - self.assertEqual(len(rel.aux_cons), 2) + self.assertEqual(len(rel.linear.cons), 2) self.assertEqual(len(rel.aux_vars), 1) self.assertAlmostEqual(rel.aux_vars[1].lb, 1) self.assertAlmostEqual(rel.aux_vars[1].ub, 8) - self.assertEqual(rel.aux_cons[1].lower, 0) - self.assertEqual(rel.aux_cons[1].upper, 0) - ders = reverse_sd(rel.aux_cons[1].body) - self.assertEqual(ders[rel.z], 1) + self.assertEqual(rel.linear.cons[1].lower, 0) + self.assertEqual(rel.linear.cons[1].upper, 0) + ders = reverse_sd(rel.linear.cons[1].body) + self.assertEqual(ders[m.z], 1) self.assertEqual(ders[rel.aux_vars[1]], 1) - self.assertEqual(ders[rel.y], 1) - self.assertEqual(len(list(identify_variables(rel.aux_cons[1].body))), 3) + self.assertEqual(ders[m.y], 1) + self.assertEqual(len(list(identify_variables(rel.linear.cons[1].body))), 3) - self.assertEqual(rel.aux_cons[2].lower, 0) - self.assertEqual(rel.aux_cons[2].upper, 0) - ders = reverse_sd(rel.aux_cons[2].body) - self.assertEqual(ders[rel.w], 1) + self.assertEqual(rel.linear.cons[2].lower, 0) + self.assertEqual(rel.linear.cons[2].upper, 0) + ders = reverse_sd(rel.linear.cons[2].body) + self.assertEqual(ders[m.w], 1) self.assertEqual(ders[rel.aux_vars[1]], -3) - self.assertEqual(len(list(identify_variables(rel.aux_cons[2].body))), 2) + self.assertEqual(len(list(identify_variables(rel.linear.cons[2].body))), 2) self.assertTrue(hasattr(rel, 'relaxations')) self.assertTrue(hasattr(rel.relaxations, 'rel0')) self.assertTrue( isinstance(rel.relaxations.rel0, coramin.relaxations.PWUnivariateRelaxation) ) - self.assertIn(rel.x, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) + self.assertIn(m.x, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) self.assertEqual(id(rel.aux_vars[1]), id(rel.relaxations.rel0.get_aux_var())) self.assertTrue(rel.relaxations.rel0.is_rhs_convex()) self.assertFalse(rel.relaxations.rel0.is_rhs_concave()) @@ -240,43 +235,42 @@ def test_cubic_convex(self): def test_cubic_concave(self): m = pe.ConcreteModel() - m.x = pe.Var(bounds=(-2, -1)) - m.y = pe.Var() - m.z = pe.Var() - m.w = pe.Var() + m.x = x = pe.Var(bounds=(-2, -1)) + m.y = y = pe.Var() + m.z = z = pe.Var() + m.w = w = pe.Var() m.c = pe.Constraint(expr=m.x**3 + m.y + m.z == 0) m.c2 = pe.Constraint(expr=m.w - 3 * m.x**3 == 0) - rel = coramin.relaxations.relax(m, use_alpha_bb=False) + rel = coramin.relaxations.relax(m) - self.assertTrue(hasattr(rel, 'aux_cons')) self.assertTrue(hasattr(rel, 'aux_vars')) - self.assertEqual(len(rel.aux_cons), 2) + self.assertEqual(len(rel.linear.cons), 2) self.assertEqual(len(rel.aux_vars), 1) self.assertAlmostEqual(rel.aux_vars[1].lb, -8) self.assertAlmostEqual(rel.aux_vars[1].ub, -1) - self.assertEqual(rel.aux_cons[1].lower, 0) - self.assertEqual(rel.aux_cons[1].upper, 0) - ders = reverse_sd(rel.aux_cons[1].body) - self.assertEqual(ders[rel.z], 1) + self.assertEqual(rel.linear.cons[1].lower, 0) + self.assertEqual(rel.linear.cons[1].upper, 0) + ders = reverse_sd(rel.linear.cons[1].body) + self.assertEqual(ders[m.z], 1) self.assertEqual(ders[rel.aux_vars[1]], 1) - self.assertEqual(ders[rel.y], 1) - self.assertEqual(len(list(identify_variables(rel.aux_cons[1].body))), 3) + self.assertEqual(ders[m.y], 1) + self.assertEqual(len(list(identify_variables(rel.linear.cons[1].body))), 3) - self.assertEqual(rel.aux_cons[2].lower, 0) - self.assertEqual(rel.aux_cons[2].upper, 0) - ders = reverse_sd(rel.aux_cons[2].body) - self.assertEqual(ders[rel.w], 1) + self.assertEqual(rel.linear.cons[2].lower, 0) + self.assertEqual(rel.linear.cons[2].upper, 0) + ders = reverse_sd(rel.linear.cons[2].body) + self.assertEqual(ders[m.w], 1) self.assertEqual(ders[rel.aux_vars[1]], -3) - self.assertEqual(len(list(identify_variables(rel.aux_cons[2].body))), 2) + self.assertEqual(len(list(identify_variables(rel.linear.cons[2].body))), 2) self.assertTrue(hasattr(rel, 'relaxations')) self.assertTrue(hasattr(rel.relaxations, 'rel0')) self.assertTrue( isinstance(rel.relaxations.rel0, coramin.relaxations.PWUnivariateRelaxation) ) - self.assertIn(rel.x, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) + self.assertIn(m.x, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) self.assertEqual(id(rel.aux_vars[1]), id(rel.relaxations.rel0.get_aux_var())) self.assertFalse(rel.relaxations.rel0.is_rhs_convex()) self.assertTrue(rel.relaxations.rel0.is_rhs_concave()) @@ -284,14 +278,14 @@ def test_cubic_concave(self): def test_cubic(self): m = pe.ConcreteModel() - m.x = pe.Var(bounds=(-1, 1)) - m.y = pe.Var() - m.z = pe.Var() - m.w = pe.Var() + m.x = x = pe.Var(bounds=(-1, 1)) + m.y = y = pe.Var() + m.z = z = pe.Var() + m.w = w = pe.Var() m.c = pe.Constraint(expr=m.x**3 + m.y + m.z == 0) m.c2 = pe.Constraint(expr=m.w - 3 * m.x**3 == 0) - rel = coramin.relaxations.relax(m, use_alpha_bb=False) + rel = coramin.relaxations.relax(m) # this problem should turn into # @@ -300,9 +294,8 @@ def test_cubic(self): # aux1 = x**2 => rel0 # aux2 = x*aux1 => rel1 - self.assertTrue(hasattr(rel, 'aux_cons')) self.assertTrue(hasattr(rel, 'aux_vars')) - self.assertEqual(len(rel.aux_cons), 2) + self.assertEqual(len(rel.linear.cons), 2) self.assertEqual(len(rel.aux_vars), 2) self.assertAlmostEqual(rel.aux_vars[1].lb, 0) @@ -311,34 +304,34 @@ def test_cubic(self): self.assertAlmostEqual(rel.aux_vars[2].lb, -1) self.assertAlmostEqual(rel.aux_vars[2].ub, 1) - self.assertEqual(rel.aux_cons[1].lower, 0) - self.assertEqual(rel.aux_cons[1].upper, 0) - ders = reverse_sd(rel.aux_cons[1].body) - self.assertEqual(ders[rel.z], 1) + self.assertEqual(rel.linear.cons[1].lower, 0) + self.assertEqual(rel.linear.cons[1].upper, 0) + ders = reverse_sd(rel.linear.cons[1].body) + self.assertEqual(ders[z], 1) self.assertEqual(ders[rel.aux_vars[2]], 1) - self.assertEqual(ders[rel.y], 1) - self.assertEqual(len(list(identify_variables(rel.aux_cons[1].body))), 3) + self.assertEqual(ders[y], 1) + self.assertEqual(len(list(identify_variables(rel.linear.cons[1].body))), 3) - self.assertEqual(rel.aux_cons[2].lower, 0) - self.assertEqual(rel.aux_cons[2].upper, 0) - ders = reverse_sd(rel.aux_cons[2].body) - self.assertEqual(ders[rel.w], 1) + self.assertEqual(rel.linear.cons[2].lower, 0) + self.assertEqual(rel.linear.cons[2].upper, 0) + ders = reverse_sd(rel.linear.cons[2].body) + self.assertEqual(ders[w], 1) self.assertEqual(ders[rel.aux_vars[2]], -3) - self.assertEqual(len(list(identify_variables(rel.aux_cons[2].body))), 2) + self.assertEqual(len(list(identify_variables(rel.linear.cons[2].body))), 2) self.assertTrue(hasattr(rel, 'relaxations')) self.assertTrue(hasattr(rel.relaxations, 'rel0')) self.assertTrue( isinstance(rel.relaxations.rel0, coramin.relaxations.PWXSquaredRelaxation) ) - self.assertIn(rel.x, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) + self.assertIn(x, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) self.assertEqual(id(rel.aux_vars[1]), id(rel.relaxations.rel0.get_aux_var())) self.assertTrue(hasattr(rel.relaxations, 'rel1')) self.assertTrue( isinstance(rel.relaxations.rel1, coramin.relaxations.PWMcCormickRelaxation) ) - self.assertIn(rel.x, ComponentSet(rel.relaxations.rel1.get_rhs_vars())) + self.assertIn(x, ComponentSet(rel.relaxations.rel1.get_rhs_vars())) self.assertIn( rel.aux_vars[1], ComponentSet(rel.relaxations.rel1.get_rhs_vars()) ) @@ -354,36 +347,35 @@ def test_pow_fractional1(self): m.c = pe.Constraint(expr=m.x**m.p + m.y + m.z == 0) m.c2 = pe.Constraint(expr=m.w - 3 * m.x**m.p == 0) - rel = coramin.relaxations.relax(m, use_alpha_bb=False) + rel = coramin.relaxations.relax(m) - self.assertTrue(hasattr(rel, 'aux_cons')) self.assertTrue(hasattr(rel, 'aux_vars')) - self.assertEqual(len(rel.aux_cons), 2) + self.assertEqual(len(rel.linear.cons), 2) self.assertEqual(len(rel.aux_vars), 1) self.assertAlmostEqual(rel.aux_vars[1].lb, 0) self.assertAlmostEqual(rel.aux_vars[1].ub, 1) - self.assertEqual(rel.aux_cons[1].lower, 0) - self.assertEqual(rel.aux_cons[1].upper, 0) - ders = reverse_sd(rel.aux_cons[1].body) - self.assertEqual(ders[rel.z], 1) + self.assertEqual(rel.linear.cons[1].lower, 0) + self.assertEqual(rel.linear.cons[1].upper, 0) + ders = reverse_sd(rel.linear.cons[1].body) + self.assertEqual(ders[m.z], 1) self.assertEqual(ders[rel.aux_vars[1]], 1) - self.assertEqual(ders[rel.y], 1) - self.assertEqual(len(list(identify_variables(rel.aux_cons[1].body))), 3) + self.assertEqual(ders[m.y], 1) + self.assertEqual(len(list(identify_variables(rel.linear.cons[1].body))), 3) - self.assertEqual(rel.aux_cons[2].lower, 0) - self.assertEqual(rel.aux_cons[2].upper, 0) - ders = reverse_sd(rel.aux_cons[2].body) - self.assertEqual(ders[rel.w], 1) + self.assertEqual(rel.linear.cons[2].lower, 0) + self.assertEqual(rel.linear.cons[2].upper, 0) + ders = reverse_sd(rel.linear.cons[2].body) + self.assertEqual(ders[m.w], 1) self.assertEqual(ders[rel.aux_vars[1]], -3) - self.assertEqual(len(list(identify_variables(rel.aux_cons[2].body))), 2) + self.assertEqual(len(list(identify_variables(rel.linear.cons[2].body))), 2) self.assertTrue(hasattr(rel, 'relaxations')) self.assertTrue(hasattr(rel.relaxations, 'rel0')) self.assertTrue( isinstance(rel.relaxations.rel0, coramin.relaxations.PWUnivariateRelaxation) ) - self.assertIn(rel.x, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) + self.assertIn(m.x, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) self.assertEqual(id(rel.aux_vars[1]), id(rel.relaxations.rel0.get_aux_var())) self.assertFalse(rel.relaxations.rel0.is_rhs_convex()) self.assertTrue(rel.relaxations.rel0.is_rhs_concave()) @@ -399,36 +391,35 @@ def test_pow_fractional2(self): m.c = pe.Constraint(expr=m.x**m.p + m.y + m.z == 0) m.c2 = pe.Constraint(expr=m.w - 3 * m.x**m.p == 0) - rel = coramin.relaxations.relax(m, use_alpha_bb=False) + rel = coramin.relaxations.relax(m) - self.assertTrue(hasattr(rel, 'aux_cons')) self.assertTrue(hasattr(rel, 'aux_vars')) - self.assertEqual(len(rel.aux_cons), 2) + self.assertEqual(len(rel.linear.cons), 2) self.assertEqual(len(rel.aux_vars), 1) self.assertAlmostEqual(rel.aux_vars[1].lb, 0) self.assertAlmostEqual(rel.aux_vars[1].ub, 1) - self.assertEqual(rel.aux_cons[1].lower, 0) - self.assertEqual(rel.aux_cons[1].upper, 0) - ders = reverse_sd(rel.aux_cons[1].body) - self.assertEqual(ders[rel.z], 1) + self.assertEqual(rel.linear.cons[1].lower, 0) + self.assertEqual(rel.linear.cons[1].upper, 0) + ders = reverse_sd(rel.linear.cons[1].body) + self.assertEqual(ders[m.z], 1) self.assertEqual(ders[rel.aux_vars[1]], 1) - self.assertEqual(ders[rel.y], 1) - self.assertEqual(len(list(identify_variables(rel.aux_cons[1].body))), 3) + self.assertEqual(ders[m.y], 1) + self.assertEqual(len(list(identify_variables(rel.linear.cons[1].body))), 3) - self.assertEqual(rel.aux_cons[2].lower, 0) - self.assertEqual(rel.aux_cons[2].upper, 0) - ders = reverse_sd(rel.aux_cons[2].body) - self.assertEqual(ders[rel.w], 1) + self.assertEqual(rel.linear.cons[2].lower, 0) + self.assertEqual(rel.linear.cons[2].upper, 0) + ders = reverse_sd(rel.linear.cons[2].body) + self.assertEqual(ders[m.w], 1) self.assertEqual(ders[rel.aux_vars[1]], -3) - self.assertEqual(len(list(identify_variables(rel.aux_cons[2].body))), 2) + self.assertEqual(len(list(identify_variables(rel.linear.cons[2].body))), 2) self.assertTrue(hasattr(rel, 'relaxations')) self.assertTrue(hasattr(rel.relaxations, 'rel0')) self.assertTrue( isinstance(rel.relaxations.rel0, coramin.relaxations.PWUnivariateRelaxation) ) - self.assertIn(rel.x, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) + self.assertIn(m.x, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) self.assertEqual(id(rel.aux_vars[1]), id(rel.relaxations.rel0.get_aux_var())) self.assertTrue(rel.relaxations.rel0.is_rhs_convex()) self.assertFalse(rel.relaxations.rel0.is_rhs_concave()) @@ -444,36 +435,35 @@ def test_pow_neg_even1(self): m.c = pe.Constraint(expr=m.x**m.p + m.y + m.z == 0) m.c2 = pe.Constraint(expr=m.w - 3 * m.x**m.p == 0) - rel = coramin.relaxations.relax(m, use_alpha_bb=False) + rel = coramin.relaxations.relax(m) - self.assertTrue(hasattr(rel, 'aux_cons')) self.assertTrue(hasattr(rel, 'aux_vars')) - self.assertEqual(len(rel.aux_cons), 2) + self.assertEqual(len(rel.linear.cons), 2) self.assertEqual(len(rel.aux_vars), 1) self.assertAlmostEqual(rel.aux_vars[1].lb, 0.25) self.assertAlmostEqual(rel.aux_vars[1].ub, 1) - self.assertEqual(rel.aux_cons[1].lower, 0) - self.assertEqual(rel.aux_cons[1].upper, 0) - ders = reverse_sd(rel.aux_cons[1].body) - self.assertEqual(ders[rel.z], 1) + self.assertEqual(rel.linear.cons[1].lower, 0) + self.assertEqual(rel.linear.cons[1].upper, 0) + ders = reverse_sd(rel.linear.cons[1].body) + self.assertEqual(ders[m.z], 1) self.assertEqual(ders[rel.aux_vars[1]], 1) - self.assertEqual(ders[rel.y], 1) - self.assertEqual(len(list(identify_variables(rel.aux_cons[1].body))), 3) + self.assertEqual(ders[m.y], 1) + self.assertEqual(len(list(identify_variables(rel.linear.cons[1].body))), 3) - self.assertEqual(rel.aux_cons[2].lower, 0) - self.assertEqual(rel.aux_cons[2].upper, 0) - ders = reverse_sd(rel.aux_cons[2].body) - self.assertEqual(ders[rel.w], 1) + self.assertEqual(rel.linear.cons[2].lower, 0) + self.assertEqual(rel.linear.cons[2].upper, 0) + ders = reverse_sd(rel.linear.cons[2].body) + self.assertEqual(ders[m.w], 1) self.assertEqual(ders[rel.aux_vars[1]], -3) - self.assertEqual(len(list(identify_variables(rel.aux_cons[2].body))), 2) + self.assertEqual(len(list(identify_variables(rel.linear.cons[2].body))), 2) self.assertTrue(hasattr(rel, 'relaxations')) self.assertTrue(hasattr(rel.relaxations, 'rel0')) self.assertTrue( isinstance(rel.relaxations.rel0, coramin.relaxations.PWUnivariateRelaxation) ) - self.assertIn(rel.x, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) + self.assertIn(m.x, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) self.assertEqual(id(rel.aux_vars[1]), id(rel.relaxations.rel0.get_aux_var())) self.assertTrue(rel.relaxations.rel0.is_rhs_convex()) self.assertFalse(rel.relaxations.rel0.is_rhs_concave()) @@ -489,36 +479,35 @@ def test_pow_neg_even2(self): m.c = pe.Constraint(expr=m.x**m.p + m.y + m.z == 0) m.c2 = pe.Constraint(expr=m.w - 3 * m.x**m.p == 0) - rel = coramin.relaxations.relax(m, use_alpha_bb=False) + rel = coramin.relaxations.relax(m) - self.assertTrue(hasattr(rel, 'aux_cons')) self.assertTrue(hasattr(rel, 'aux_vars')) - self.assertEqual(len(rel.aux_cons), 2) + self.assertEqual(len(rel.linear.cons), 2) self.assertEqual(len(rel.aux_vars), 1) self.assertAlmostEqual(rel.aux_vars[1].lb, 0.25) self.assertAlmostEqual(rel.aux_vars[1].ub, 1) - self.assertEqual(rel.aux_cons[1].lower, 0) - self.assertEqual(rel.aux_cons[1].upper, 0) - ders = reverse_sd(rel.aux_cons[1].body) - self.assertEqual(ders[rel.z], 1) + self.assertEqual(rel.linear.cons[1].lower, 0) + self.assertEqual(rel.linear.cons[1].upper, 0) + ders = reverse_sd(rel.linear.cons[1].body) + self.assertEqual(ders[m.z], 1) self.assertEqual(ders[rel.aux_vars[1]], 1) - self.assertEqual(ders[rel.y], 1) - self.assertEqual(len(list(identify_variables(rel.aux_cons[1].body))), 3) + self.assertEqual(ders[m.y], 1) + self.assertEqual(len(list(identify_variables(rel.linear.cons[1].body))), 3) - self.assertEqual(rel.aux_cons[2].lower, 0) - self.assertEqual(rel.aux_cons[2].upper, 0) - ders = reverse_sd(rel.aux_cons[2].body) - self.assertEqual(ders[rel.w], 1) + self.assertEqual(rel.linear.cons[2].lower, 0) + self.assertEqual(rel.linear.cons[2].upper, 0) + ders = reverse_sd(rel.linear.cons[2].body) + self.assertEqual(ders[m.w], 1) self.assertEqual(ders[rel.aux_vars[1]], -3) - self.assertEqual(len(list(identify_variables(rel.aux_cons[2].body))), 2) + self.assertEqual(len(list(identify_variables(rel.linear.cons[2].body))), 2) self.assertTrue(hasattr(rel, 'relaxations')) self.assertTrue(hasattr(rel.relaxations, 'rel0')) self.assertTrue( isinstance(rel.relaxations.rel0, coramin.relaxations.PWUnivariateRelaxation) ) - self.assertIn(rel.x, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) + self.assertIn(m.x, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) self.assertEqual(id(rel.aux_vars[1]), id(rel.relaxations.rel0.get_aux_var())) self.assertTrue(rel.relaxations.rel0.is_rhs_convex()) self.assertFalse(rel.relaxations.rel0.is_rhs_concave()) @@ -534,36 +523,35 @@ def test_pow_neg_odd1(self): m.c = pe.Constraint(expr=m.x**m.p + m.y + m.z == 0) m.c2 = pe.Constraint(expr=m.w - 3 * m.x**m.p == 0) - rel = coramin.relaxations.relax(m, use_alpha_bb=False) + rel = coramin.relaxations.relax(m) - self.assertTrue(hasattr(rel, 'aux_cons')) self.assertTrue(hasattr(rel, 'aux_vars')) - self.assertEqual(len(rel.aux_cons), 2) + self.assertEqual(len(rel.linear.cons), 2) self.assertEqual(len(rel.aux_vars), 1) self.assertAlmostEqual(rel.aux_vars[1].lb, 0.125) self.assertAlmostEqual(rel.aux_vars[1].ub, 1) - self.assertEqual(rel.aux_cons[1].lower, 0) - self.assertEqual(rel.aux_cons[1].upper, 0) - ders = reverse_sd(rel.aux_cons[1].body) - self.assertEqual(ders[rel.z], 1) + self.assertEqual(rel.linear.cons[1].lower, 0) + self.assertEqual(rel.linear.cons[1].upper, 0) + ders = reverse_sd(rel.linear.cons[1].body) + self.assertEqual(ders[m.z], 1) self.assertEqual(ders[rel.aux_vars[1]], 1) - self.assertEqual(ders[rel.y], 1) - self.assertEqual(len(list(identify_variables(rel.aux_cons[1].body))), 3) + self.assertEqual(ders[m.y], 1) + self.assertEqual(len(list(identify_variables(rel.linear.cons[1].body))), 3) - self.assertEqual(rel.aux_cons[2].lower, 0) - self.assertEqual(rel.aux_cons[2].upper, 0) - ders = reverse_sd(rel.aux_cons[2].body) - self.assertEqual(ders[rel.w], 1) + self.assertEqual(rel.linear.cons[2].lower, 0) + self.assertEqual(rel.linear.cons[2].upper, 0) + ders = reverse_sd(rel.linear.cons[2].body) + self.assertEqual(ders[m.w], 1) self.assertEqual(ders[rel.aux_vars[1]], -3) - self.assertEqual(len(list(identify_variables(rel.aux_cons[2].body))), 2) + self.assertEqual(len(list(identify_variables(rel.linear.cons[2].body))), 2) self.assertTrue(hasattr(rel, 'relaxations')) self.assertTrue(hasattr(rel.relaxations, 'rel0')) self.assertTrue( isinstance(rel.relaxations.rel0, coramin.relaxations.PWUnivariateRelaxation) ) - self.assertIn(rel.x, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) + self.assertIn(m.x, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) self.assertEqual(id(rel.aux_vars[1]), id(rel.relaxations.rel0.get_aux_var())) self.assertTrue(rel.relaxations.rel0.is_rhs_convex()) self.assertFalse(rel.relaxations.rel0.is_rhs_concave()) @@ -579,36 +567,35 @@ def test_pow_neg_odd2(self): m.c = pe.Constraint(expr=m.x**m.p + m.y + m.z == 0) m.c2 = pe.Constraint(expr=m.w - 3 * m.x**m.p == 0) - rel = coramin.relaxations.relax(m, use_alpha_bb=False) + rel = coramin.relaxations.relax(m) - self.assertTrue(hasattr(rel, 'aux_cons')) self.assertTrue(hasattr(rel, 'aux_vars')) - self.assertEqual(len(rel.aux_cons), 2) + self.assertEqual(len(rel.linear.cons), 2) self.assertEqual(len(rel.aux_vars), 1) self.assertAlmostEqual(rel.aux_vars[1].lb, -1) self.assertAlmostEqual(rel.aux_vars[1].ub, -0.125) - self.assertEqual(rel.aux_cons[1].lower, 0) - self.assertEqual(rel.aux_cons[1].upper, 0) - ders = reverse_sd(rel.aux_cons[1].body) - self.assertEqual(ders[rel.z], 1) + self.assertEqual(rel.linear.cons[1].lower, 0) + self.assertEqual(rel.linear.cons[1].upper, 0) + ders = reverse_sd(rel.linear.cons[1].body) + self.assertEqual(ders[m.z], 1) self.assertEqual(ders[rel.aux_vars[1]], 1) - self.assertEqual(ders[rel.y], 1) - self.assertEqual(len(list(identify_variables(rel.aux_cons[1].body))), 3) + self.assertEqual(ders[m.y], 1) + self.assertEqual(len(list(identify_variables(rel.linear.cons[1].body))), 3) - self.assertEqual(rel.aux_cons[2].lower, 0) - self.assertEqual(rel.aux_cons[2].upper, 0) - ders = reverse_sd(rel.aux_cons[2].body) - self.assertEqual(ders[rel.w], 1) + self.assertEqual(rel.linear.cons[2].lower, 0) + self.assertEqual(rel.linear.cons[2].upper, 0) + ders = reverse_sd(rel.linear.cons[2].body) + self.assertEqual(ders[m.w], 1) self.assertEqual(ders[rel.aux_vars[1]], -3) - self.assertEqual(len(list(identify_variables(rel.aux_cons[2].body))), 2) + self.assertEqual(len(list(identify_variables(rel.linear.cons[2].body))), 2) self.assertTrue(hasattr(rel, 'relaxations')) self.assertTrue(hasattr(rel.relaxations, 'rel0')) self.assertTrue( isinstance(rel.relaxations.rel0, coramin.relaxations.PWUnivariateRelaxation) ) - self.assertIn(rel.x, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) + self.assertIn(m.x, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) self.assertEqual(id(rel.aux_vars[1]), id(rel.relaxations.rel0.get_aux_var())) self.assertFalse(rel.relaxations.rel0.is_rhs_convex()) self.assertTrue(rel.relaxations.rel0.is_rhs_concave()) @@ -624,7 +611,7 @@ def test_pow_neg(self): m.c = pe.Constraint(expr=m.x**m.p + m.y + m.z == 0) m.c2 = pe.Constraint(expr=m.w - 3 * m.x**m.p == 0) - rel = coramin.relaxations.relax(m, use_alpha_bb=False) + rel = coramin.relaxations.relax(m) # This model should be relaxed to # @@ -635,9 +622,8 @@ def test_pow_neg(self): # aux3 = 1 # - self.assertTrue(hasattr(rel, 'aux_cons')) self.assertTrue(hasattr(rel, 'aux_vars')) - self.assertEqual(len(rel.aux_cons), 2) + self.assertEqual(len(rel.linear.cons), 2) self.assertEqual(len(rel.aux_vars), 3) self.assertAlmostEqual(rel.aux_vars[1].lb, 0) @@ -646,27 +632,27 @@ def test_pow_neg(self): self.assertTrue(rel.aux_vars[3].is_fixed()) self.assertEqual(rel.aux_vars[3].value, 1) - self.assertEqual(rel.aux_cons[1].lower, 0) - self.assertEqual(rel.aux_cons[1].upper, 0) - ders = reverse_sd(rel.aux_cons[1].body) - self.assertEqual(ders[rel.z], 1) + self.assertEqual(rel.linear.cons[1].lower, 0) + self.assertEqual(rel.linear.cons[1].upper, 0) + ders = reverse_sd(rel.linear.cons[1].body) + self.assertEqual(ders[m.z], 1) self.assertEqual(ders[rel.aux_vars[2]], 1) - self.assertEqual(ders[rel.y], 1) - self.assertEqual(len(list(identify_variables(rel.aux_cons[1].body))), 3) + self.assertEqual(ders[m.y], 1) + self.assertEqual(len(list(identify_variables(rel.linear.cons[1].body))), 3) - self.assertEqual(rel.aux_cons[2].lower, 0) - self.assertEqual(rel.aux_cons[2].upper, 0) - ders = reverse_sd(rel.aux_cons[2].body) - self.assertEqual(ders[rel.w], 1) + self.assertEqual(rel.linear.cons[2].lower, 0) + self.assertEqual(rel.linear.cons[2].upper, 0) + ders = reverse_sd(rel.linear.cons[2].body) + self.assertEqual(ders[m.w], 1) self.assertEqual(ders[rel.aux_vars[2]], -3) - self.assertEqual(len(list(identify_variables(rel.aux_cons[2].body))), 2) + self.assertEqual(len(list(identify_variables(rel.linear.cons[2].body))), 2) self.assertTrue(hasattr(rel, 'relaxations')) self.assertTrue(hasattr(rel.relaxations, 'rel0')) self.assertTrue( isinstance(rel.relaxations.rel0, coramin.relaxations.PWXSquaredRelaxation) ) - self.assertIn(rel.x, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) + self.assertIn(m.x, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) self.assertEqual(id(rel.aux_vars[1]), id(rel.relaxations.rel0.get_aux_var())) self.assertTrue(rel.relaxations.rel0.is_rhs_convex()) self.assertFalse(rel.relaxations.rel0.is_rhs_concave()) @@ -690,7 +676,8 @@ def test_sqrt(self): m.x = pe.Var() m.z = pe.Var() m.c = pe.Constraint(expr=m.z + pe.sqrt(2 * pe.log(m.x)) <= 1) - coramin.relaxations.relax(m, in_place=True, use_fbbt=False, use_alpha_bb=False) + orig = m + m = coramin.relaxations.relax(m) rels = list( coramin.relaxations.relaxation_data_objects( m, descend_into=True, active=True, sort=True @@ -699,22 +686,22 @@ def test_sqrt(self): self.assertEqual(len(rels), 2) rel0 = m.relaxations.rel0 # log rel1 = m.relaxations.rel1 # sqrt - self.assertEqual(sympyify_expression(rel0.get_rhs_expr() - pe.log(m.x))[1], 0) + self.assertEqual(sympyify_expression(rel0.get_rhs_expr() - pe.log(orig.x))[1], 0) self.assertEqual( sympyify_expression(rel1.get_rhs_expr() - m.aux_vars[3] ** 0.5)[1], 0 ) self.assertEqual( - sympyify_expression(m.aux_cons[1].body - m.aux_vars[3] + 2 * m.aux_vars[1])[ + sympyify_expression(m.linear.cons[1].body - m.aux_vars[3] + 2 * m.aux_vars[1])[ 1 ], 0, ) self.assertEqual( - sympyify_expression(m.aux_cons[2].body - m.z - m.aux_vars[2])[1], 0 + sympyify_expression(m.linear.cons[2].body - orig.z - m.aux_vars[2])[1], 0 ) - self.assertEqual(m.aux_cons[1].lower, 0) - self.assertEqual(m.aux_cons[2].lower, None) - self.assertEqual(m.aux_cons[2].upper, 1) + self.assertEqual(m.linear.cons[1].lower, 0) + self.assertEqual(m.linear.cons[2].lower, None) + self.assertEqual(m.linear.cons[2].upper, 1) self.assertIs(rel0.get_aux_var(), m.aux_vars[1]) self.assertIs(rel1.get_aux_var(), m.aux_vars[2]) self.assertEqual(rel0.relaxation_side, coramin.utils.RelaxationSide.UNDER) @@ -729,11 +716,10 @@ def test_exp(self): m.c = pe.Constraint(expr=pe.exp(m.x * m.y) + m.z == 0) m.c2 = pe.Constraint(expr=m.w - 3 * pe.exp(m.x * m.y) == 0) - rel = coramin.relaxations.relax(m, use_alpha_bb=False) + rel = coramin.relaxations.relax(m) - self.assertTrue(hasattr(rel, 'aux_cons')) self.assertTrue(hasattr(rel, 'aux_vars')) - self.assertEqual(len(rel.aux_cons), 2) + self.assertEqual(len(rel.linear.cons), 2) self.assertEqual(len(rel.aux_vars), 2) self.assertAlmostEqual(rel.aux_vars[1].lb, -1) @@ -742,27 +728,27 @@ def test_exp(self): self.assertAlmostEqual(rel.aux_vars[2].lb, math.exp(-1)) self.assertAlmostEqual(rel.aux_vars[2].ub, math.exp(1)) - self.assertEqual(rel.aux_cons[1].lower, 0) - self.assertEqual(rel.aux_cons[1].upper, 0) - ders = reverse_sd(rel.aux_cons[1].body) - self.assertEqual(ders[rel.z], 1) + self.assertEqual(rel.linear.cons[1].lower, 0) + self.assertEqual(rel.linear.cons[1].upper, 0) + ders = reverse_sd(rel.linear.cons[1].body) + self.assertEqual(ders[m.z], 1) self.assertEqual(ders[rel.aux_vars[2]], 1) - self.assertEqual(len(list(identify_variables(rel.aux_cons[1].body))), 2) + self.assertEqual(len(list(identify_variables(rel.linear.cons[1].body))), 2) - self.assertEqual(rel.aux_cons[2].lower, 0) - self.assertEqual(rel.aux_cons[2].upper, 0) - ders = reverse_sd(rel.aux_cons[2].body) - self.assertEqual(ders[rel.w], 1) + self.assertEqual(rel.linear.cons[2].lower, 0) + self.assertEqual(rel.linear.cons[2].upper, 0) + ders = reverse_sd(rel.linear.cons[2].body) + self.assertEqual(ders[m.w], 1) self.assertEqual(ders[rel.aux_vars[2]], -3) - self.assertEqual(len(list(identify_variables(rel.aux_cons[2].body))), 2) + self.assertEqual(len(list(identify_variables(rel.linear.cons[2].body))), 2) self.assertTrue(hasattr(rel, 'relaxations')) self.assertTrue(hasattr(rel.relaxations, 'rel0')) self.assertTrue( isinstance(rel.relaxations.rel0, coramin.relaxations.PWMcCormickRelaxation) ) - self.assertIn(rel.x, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) - self.assertIn(rel.y, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) + self.assertIn(m.x, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) + self.assertIn(m.y, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) self.assertEqual(id(rel.aux_vars[1]), id(rel.relaxations.rel0.get_aux_var())) self.assertTrue(hasattr(rel.relaxations, 'rel1')) @@ -785,11 +771,10 @@ def test_log(self): m.c = pe.Constraint(expr=pe.log(m.x * m.y) + m.z == 0) m.c2 = pe.Constraint(expr=m.w - 3 * pe.log(m.x * m.y) == 0) - rel = coramin.relaxations.relax(m, use_alpha_bb=False) + rel = coramin.relaxations.relax(m) - self.assertTrue(hasattr(rel, 'aux_cons')) self.assertTrue(hasattr(rel, 'aux_vars')) - self.assertEqual(len(rel.aux_cons), 2) + self.assertEqual(len(rel.linear.cons), 2) self.assertEqual(len(rel.aux_vars), 2) self.assertAlmostEqual(rel.aux_vars[1].lb, 1) @@ -798,27 +783,27 @@ def test_log(self): self.assertAlmostEqual(rel.aux_vars[2].lb, math.log(1)) self.assertAlmostEqual(rel.aux_vars[2].ub, math.log(4)) - self.assertEqual(rel.aux_cons[1].lower, 0) - self.assertEqual(rel.aux_cons[1].upper, 0) - ders = reverse_sd(rel.aux_cons[1].body) - self.assertEqual(ders[rel.z], 1) + self.assertEqual(rel.linear.cons[1].lower, 0) + self.assertEqual(rel.linear.cons[1].upper, 0) + ders = reverse_sd(rel.linear.cons[1].body) + self.assertEqual(ders[m.z], 1) self.assertEqual(ders[rel.aux_vars[2]], 1) - self.assertEqual(len(list(identify_variables(rel.aux_cons[1].body))), 2) + self.assertEqual(len(list(identify_variables(rel.linear.cons[1].body))), 2) - self.assertEqual(rel.aux_cons[2].lower, 0) - self.assertEqual(rel.aux_cons[2].upper, 0) - ders = reverse_sd(rel.aux_cons[2].body) - self.assertEqual(ders[rel.w], 1) + self.assertEqual(rel.linear.cons[2].lower, 0) + self.assertEqual(rel.linear.cons[2].upper, 0) + ders = reverse_sd(rel.linear.cons[2].body) + self.assertEqual(ders[m.w], 1) self.assertEqual(ders[rel.aux_vars[2]], -3) - self.assertEqual(len(list(identify_variables(rel.aux_cons[2].body))), 2) + self.assertEqual(len(list(identify_variables(rel.linear.cons[2].body))), 2) self.assertTrue(hasattr(rel, 'relaxations')) self.assertTrue(hasattr(rel.relaxations, 'rel0')) self.assertTrue( isinstance(rel.relaxations.rel0, coramin.relaxations.PWMcCormickRelaxation) ) - self.assertIn(rel.x, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) - self.assertIn(rel.y, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) + self.assertIn(m.x, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) + self.assertIn(m.y, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) self.assertEqual(id(rel.aux_vars[1]), id(rel.relaxations.rel0.get_aux_var())) self.assertTrue(hasattr(rel.relaxations, 'rel1')) @@ -840,31 +825,26 @@ def test_div1(self): m.y = pe.Var(bounds=(1, 2)) m.z = pe.Var() m.c = pe.Constraint(expr=m.z - m.x / m.y == 0) - rel = coramin.relaxations.relax(m, in_place=True, use_alpha_bb=False) - self.assertIs(m, rel) - relaxations = list(coramin.relaxations.relaxation_data_objects(m)) + rel = coramin.relaxations.relax(m) + relaxations = list(coramin.relaxations.relaxation_data_objects(rel)) constraints = list( coramin.relaxations.nonrelaxation_component_data_objects( - m, ctype=pe.Constraint + rel, ctype=pe.Constraint ) ) - vars = list( - coramin.relaxations.nonrelaxation_component_data_objects(m, ctype=pe.Var) - ) self.assertEqual(len(relaxations), 1) self.assertEqual(len(constraints), 1) - self.assertEqual(len(vars), 4) r = relaxations[0] c = constraints[0] self.assertIsInstance(r, coramin.relaxations.PWMcCormickRelaxationData) c_vars = ComponentSet(identify_variables(c.body)) self.assertEqual(len(c_vars), 2) self.assertEqual(len(rel.aux_vars), 1) - self.assertIn(m.aux_vars[1], c_vars) + self.assertIn(rel.aux_vars[1], c_vars) self.assertIn(m.z, c_vars) r_vars = ComponentSet(r.get_rhs_vars()) self.assertIn(m.y, r_vars) - self.assertIn(m.aux_vars[1], r_vars) + self.assertIn(rel.aux_vars[1], r_vars) self.assertIs(r.get_aux_var(), m.x) def test_div2(self): @@ -874,28 +854,27 @@ def test_div2(self): m.z = pe.Var() m.c = pe.Constraint(expr=m.z - m.x * m.y / 2 == 0) - rel = coramin.relaxations.relax(m, use_alpha_bb=False) + rel = coramin.relaxations.relax(m) - self.assertTrue(hasattr(rel, 'aux_cons')) self.assertTrue(hasattr(rel, 'aux_vars')) - self.assertEqual(len(rel.aux_cons), 1) + self.assertEqual(len(rel.linear.cons), 1) self.assertEqual(len(rel.aux_vars), 1) self.assertAlmostEqual(rel.aux_vars[1].lb, -1) self.assertAlmostEqual(rel.aux_vars[1].ub, 1) - self.assertEqual(rel.aux_cons[1].lower, 0) - self.assertEqual(rel.aux_cons[1].upper, 0) - ders = reverse_sd(rel.aux_cons[1].body) - self.assertEqual(ders[rel.z], 1) + self.assertEqual(rel.linear.cons[1].lower, 0) + self.assertEqual(rel.linear.cons[1].upper, 0) + ders = reverse_sd(rel.linear.cons[1].body) + self.assertEqual(ders[m.z], 1) self.assertEqual(ders[rel.aux_vars[1]], -0.5) - self.assertEqual(len(list(identify_variables(rel.aux_cons[1].body))), 2) + self.assertEqual(len(list(identify_variables(rel.linear.cons[1].body))), 2) self.assertTrue(hasattr(rel, 'relaxations')) self.assertTrue(hasattr(rel.relaxations, 'rel0')) self.assertTrue( isinstance(rel.relaxations.rel0, coramin.relaxations.PWMcCormickRelaxation) ) - self.assertIn(rel.x, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) - self.assertIn(rel.y, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) + self.assertIn(m.x, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) + self.assertIn(m.y, ComponentSet(rel.relaxations.rel0.get_rhs_vars())) self.assertEqual(id(rel.aux_vars[1]), id(rel.relaxations.rel0.get_aux_var())) relaxations = list(coramin.relaxations.relaxation_data_objects(rel)) self.assertEqual(len(relaxations), 1) @@ -908,20 +887,15 @@ def test_div3(self): m.w = pe.Var() m.c = pe.Constraint(expr=m.z - m.x / m.y == 0) m.c2 = pe.Constraint(expr=m.w - m.x / m.y == 0) - rel = coramin.relaxations.relax(m, in_place=True, use_alpha_bb=False) - self.assertIs(m, rel) - relaxations = list(coramin.relaxations.relaxation_data_objects(m)) + rel = coramin.relaxations.relax(m) + relaxations = list(coramin.relaxations.relaxation_data_objects(rel)) constraints = list( coramin.relaxations.nonrelaxation_component_data_objects( - m, ctype=pe.Constraint + rel, ctype=pe.Constraint ) ) - vars = list( - coramin.relaxations.nonrelaxation_component_data_objects(m, ctype=pe.Var) - ) self.assertEqual(len(relaxations), 1) self.assertEqual(len(constraints), 2) - self.assertEqual(len(vars), 5) r = relaxations[0] c1 = constraints[0] c2 = constraints[1] @@ -931,13 +905,13 @@ def test_div3(self): self.assertEqual(len(c1_vars), 2) self.assertEqual(len(c2_vars), 2) self.assertEqual(len(rel.aux_vars), 1) - self.assertIn(m.aux_vars[1], c1_vars) - self.assertIn(m.aux_vars[1], c2_vars) + self.assertIn(rel.aux_vars[1], c1_vars) + self.assertIn(rel.aux_vars[1], c2_vars) self.assertTrue(m.z in c1_vars or m.z in c2_vars) self.assertTrue(m.w in c1_vars or m.w in c2_vars) r_vars = ComponentSet(r.get_rhs_vars()) self.assertIn(m.y, r_vars) - self.assertIn(m.aux_vars[1], r_vars) + self.assertIn(rel.aux_vars[1], r_vars) self.assertIs(r.get_aux_var(), m.x) @@ -996,88 +970,74 @@ def helper(self, func, bounds_list): RelaxationSide.OVER, RelaxationSide.BOTH, ]: - for simplification, use_alpha_bb, eigenvalue_bounder in [ - (True, True, EigenValueBounder.Gershgorin), - (True, True, EigenValueBounder.GershgorinWithSimplification), - (False, True, EigenValueBounder.GershgorinWithSimplification), - (False, False, None), - (True, True, EigenValueBounder.LinearProgram), - ]: - for lb, ub in bounds_list: - m = pe.ConcreteModel() - m.x = pe.Var(bounds=(lb, ub)) - m.aux = pe.Var() - expr = func(m.x) - if relaxation_side == coramin.utils.RelaxationSide.BOTH: - m.c = pe.Constraint(expr=m.aux == expr) - elif relaxation_side == coramin.utils.RelaxationSide.UNDER: - m.c = pe.Constraint(expr=m.aux >= expr) - elif relaxation_side == coramin.utils.RelaxationSide.OVER: - m.c = pe.Constraint(expr=m.aux <= expr) - coramin.relaxations.relax( - m, - in_place=True, - perform_expression_simplification=simplification, - use_alpha_bb=use_alpha_bb, - eigenvalue_bounder=eigenvalue_bounder, - eigenvalue_opt=appsi.solvers.Gurobi(), + for lb, ub in bounds_list: + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(lb, ub)) + m.aux = pe.Var() + expr = func(m.x) + if relaxation_side == coramin.utils.RelaxationSide.BOTH: + m.c = pe.Constraint(expr=m.aux == expr) + elif relaxation_side == coramin.utils.RelaxationSide.UNDER: + m.c = pe.Constraint(expr=m.aux >= expr) + elif relaxation_side == coramin.utils.RelaxationSide.OVER: + m.c = pe.Constraint(expr=m.aux <= expr) + coramin.relaxations.relax(m) + opt = appsi.solvers.Gurobi() + all_vars = list(m.component_data_objects(pe.Var, descend_into=True)) + m.obj = pe.Objective(expr=sum(i**2 for i in all_vars)) + + # make sure the original curve is feasible for the relaxation + for _x in [float(i) for i in np.linspace(lb, ub, 10)]: + m.x.fix(_x) + m.aux.fix(pe.value(expr)) + res = opt.solve(m) + self.assertEqual( + res.termination_condition, + appsi.base.TerminationCondition.optimal, ) - opt = appsi.solvers.Gurobi() - all_vars = list(m.component_data_objects(pe.Var, descend_into=True)) - m.obj = pe.Objective(expr=sum(i**2 for i in all_vars)) - - # make sure the original curve is feasible for the relaxation - for _x in [float(i) for i in np.linspace(lb, ub, 10)]: - m.x.fix(_x) - m.aux.fix(pe.value(expr)) + if relaxation_side == coramin.utils.RelaxationSide.UNDER: + m.aux.fix(max(pe.value(func(lb)), pe.value(func(ub))) + 1) + res = opt.solve(m) + self.assertEqual( + res.termination_condition, + appsi.base.TerminationCondition.optimal, + ) + elif relaxation_side == coramin.utils.RelaxationSide.OVER: + m.aux.fix(min(pe.value(func(lb)), pe.value(func(ub))) - 1) res = opt.solve(m) self.assertEqual( res.termination_condition, appsi.base.TerminationCondition.optimal, ) - if relaxation_side == coramin.utils.RelaxationSide.UNDER: - m.aux.fix(max(pe.value(func(lb)), pe.value(func(ub))) + 1) - res = opt.solve(m) - self.assertEqual( - res.termination_condition, - appsi.base.TerminationCondition.optimal, - ) - elif relaxation_side == coramin.utils.RelaxationSide.OVER: - m.aux.fix(min(pe.value(func(lb)), pe.value(func(ub))) - 1) - res = opt.solve(m) - self.assertEqual( - res.termination_condition, - appsi.base.TerminationCondition.optimal, - ) - - # ensure the relaxation is exact at the bounds of x - m.aux.unfix() - del m.obj - m.obj = pe.Objective(expr=m.aux) - for _x in [lb, ub]: - m.x.fix(_x) - if relaxation_side in { - coramin.utils.RelaxationSide.BOTH, - coramin.utils.RelaxationSide.UNDER, - }: - m.obj.sense = pe.minimize - res = opt.solve(m) - self.assertEqual( - res.termination_condition, - appsi.base.TerminationCondition.optimal, - ) - self.assertAlmostEqual(m.aux.value, pe.value(func(_x))) - if relaxation_side in { - coramin.utils.RelaxationSide.BOTH, - coramin.utils.RelaxationSide.OVER, - }: - m.obj.sense = pe.maximize - res = opt.solve(m) - self.assertEqual( - res.termination_condition, - appsi.base.TerminationCondition.optimal, - ) - self.assertAlmostEqual(m.aux.value, pe.value(func(_x)), 5) + + # ensure the relaxation is exact at the bounds of x + m.aux.unfix() + del m.obj + m.obj = pe.Objective(expr=m.aux) + for _x in [lb, ub]: + m.x.fix(_x) + if relaxation_side in { + coramin.utils.RelaxationSide.BOTH, + coramin.utils.RelaxationSide.UNDER, + }: + m.obj.sense = pe.minimize + res = opt.solve(m) + self.assertEqual( + res.termination_condition, + appsi.base.TerminationCondition.optimal, + ) + self.assertAlmostEqual(m.aux.value, pe.value(func(_x))) + if relaxation_side in { + coramin.utils.RelaxationSide.BOTH, + coramin.utils.RelaxationSide.OVER, + }: + m.obj.sense = pe.maximize + res = opt.solve(m) + self.assertEqual( + res.termination_condition, + appsi.base.TerminationCondition.optimal, + ) + self.assertAlmostEqual(m.aux.value, pe.value(func(_x)), 5) def test_exp(self): self.helper(func=pe.exp, bounds_list=[(-1, 1)]) @@ -1145,7 +1105,8 @@ def helper(self, func, lb, ub): m.aux2 = pe.Var() m.c1 = pe.Constraint(expr=m.aux1 <= 2 * func(m.x) + 3) m.c2 = pe.Constraint(expr=m.aux2 >= 3 * func(m.x) + 2) - coramin.relaxations.relax(m, in_place=True, use_alpha_bb=False) + orig = m + m = coramin.relaxations.relax(m) rels = list(coramin.relaxations.relaxation_data_objects(m)) self.assertEqual(len(rels), 1) r = rels[0] @@ -1183,20 +1144,15 @@ def helper(self, func, param_val): m.aux = pe.Var() m.p = pe.Param(mutable=True, initialize=param_val) m.c = pe.Constraint(expr=m.aux == func(m.p) * m.x**2) - self.assertIn( - m.p, ComponentSet(identify_components(m.c.body, [_ParamData, ScalarParam])) - ) - coramin.relaxations.relax(m, in_place=True, use_alpha_bb=False) + orig = m + m = coramin.relaxations.relax(m) rels = list(coramin.relaxations.relaxation_data_objects(m)) self.assertEqual(len(rels), 1) r = rels[0] self.assertIsInstance(r, coramin.relaxations.PWXSquaredRelaxationData) - self.assertIn( - m.p, - ComponentSet( - identify_components(m.aux_cons[1].body, [_ParamData, ScalarParam]) - ), - ) + assertExpressionsEqual(self, m.linear.cons[1].body, orig.aux - func(param_val)*m.aux_vars[1]) + self.assertEqual(m.linear.cons[1].lb, 0) + self.assertEqual(m.linear.cons[1].ub, 0) def test_exp(self): self.helper(func=pe.exp, param_val=1) From 1e8269404d3fcada2f77f7a6a61d882e1c6db243 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 24 Jan 2024 03:40:44 -0700 Subject: [PATCH 081/128] coramin: update tests --- .../coramin/relaxations/tests/test_relaxations.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/coramin/relaxations/tests/test_relaxations.py b/pyomo/contrib/coramin/relaxations/tests/test_relaxations.py index 5eab5e7f257..155b5d468de 100644 --- a/pyomo/contrib/coramin/relaxations/tests/test_relaxations.py +++ b/pyomo/contrib/coramin/relaxations/tests/test_relaxations.py @@ -105,7 +105,11 @@ def _check_unbounded( sense = pe.maximize m.obj = pe.Objective(expr=rel.get_aux_var(), sense=sense) + unfixed_vars = list() for v in rel.get_rhs_vars(): + if v.is_fixed(): + continue + unfixed_vars.append(v) if v.has_lb() and v.has_ub(): v.fix(0.5 * (v.lb + v.ub)) elif v.has_lb(): @@ -122,6 +126,9 @@ def _check_unbounded( del m.obj + for v in unfixed_vars: + v.unfix() + return res.termination_condition == appsi.base.TerminationCondition.unbounded @@ -201,7 +208,7 @@ def valid_relaxation_helper( check_underestimator: bool = True, check_overestimator: bool = True, ): - if rel.use_linear_relaxation: + if rel.use_linear_relaxation and all(v.lb != v.ub for v in rel.get_rhs_vars()): self.assertTrue(_check_linear(m)) rhs_vars = rel.get_rhs_vars() sample_points = _grid_rhs_vars(rhs_vars, num_points=num_points) From 7b9760a474f5462f850bb6dc8218326140401e49 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 24 Jan 2024 07:23:30 -0700 Subject: [PATCH 082/128] coramin: update tests --- .../coramin/algorithms/multitree/tests/test_multitree.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/coramin/algorithms/multitree/tests/test_multitree.py b/pyomo/contrib/coramin/algorithms/multitree/tests/test_multitree.py index 03540b0bb94..8fc1fd9e501 100644 --- a/pyomo/contrib/coramin/algorithms/multitree/tests/test_multitree.py +++ b/pyomo/contrib/coramin/algorithms/multitree/tests/test_multitree.py @@ -13,6 +13,7 @@ import pyomo.environ as pe from pyomo.core.base.block import _BlockData import importlib +import shutil def _get_sol(pname): @@ -79,15 +80,15 @@ def setUpClass(self) -> None: def tearDownClass(self) -> None: current_dir = os.getcwd() for pname in self.test_problems.keys(): - os.remove(os.path.join(current_dir, 'minlplib', 'osil', f'{pname}.osil')) - os.rmdir(os.path.join(current_dir, 'minlplib', 'osil')) + os.remove(os.path.join(current_dir, 'minlplib', 'py', f'{pname}.py')) + shutil.rmtree(os.path.join(current_dir, 'minlplib', 'py')) os.rmdir(os.path.join(current_dir, 'minlplib')) for pname in self.primal_sol.keys(): os.remove(os.path.join(current_dir, f'{pname}.sol')) def get_model(self, pname): current_dir = os.getcwd() - fname = os.path.join(current_dir, 'minlplib', 'py', f'{pname}') + fname = os.path.join('minlplib', 'py', f'{pname}') fname = fname.replace('/', '.') m = importlib.import_module(fname).m return m From 2203c808f431f265b255d7c486e4a9e847a5bb10 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Thu, 25 Jan 2024 19:46:22 -0700 Subject: [PATCH 083/128] coramin: cleanup --- pyomo/contrib/coramin/algorithms/alg_utils.py | 77 ++++++ pyomo/contrib/coramin/algorithms/bnb/bnb.py | 174 ++----------- pyomo/contrib/coramin/algorithms/cut_gen.py | 41 +++ .../coramin/algorithms/multitree/multitree.py | 239 +++++++----------- pyomo/contrib/coramin/clone.py | 29 ++- .../coramin/cutting_planes/alpha_bb_cuts.py | 35 ++- pyomo/contrib/coramin/cutting_planes/base.py | 2 +- pyomo/contrib/coramin/heuristics/diving.py | 4 +- .../contrib/coramin/relaxations/auto_relax.py | 4 +- 9 files changed, 292 insertions(+), 313 deletions(-) create mode 100644 pyomo/contrib/coramin/algorithms/alg_utils.py create mode 100644 pyomo/contrib/coramin/algorithms/cut_gen.py diff --git a/pyomo/contrib/coramin/algorithms/alg_utils.py b/pyomo/contrib/coramin/algorithms/alg_utils.py new file mode 100644 index 00000000000..3647fdfa934 --- /dev/null +++ b/pyomo/contrib/coramin/algorithms/alg_utils.py @@ -0,0 +1,77 @@ +import pyomo.environ as pe +from pyomo.repn.standard_repn import generate_standard_repn, StandardRepn +from pyomo.contrib.coramin.relaxations.split_expr import split_expr +from pyomo.core.expr.numeric_expr import LinearExpression +from pyomo.core.base.var import _GeneralVarData +from pyomo.core.base.block import _BlockData +from pyomo.common.collections import ComponentSet +from typing import Tuple, List, Sequence + + +def collect_vars(m: _BlockData) -> Tuple[List[_GeneralVarData], List[_GeneralVarData]]: + binary_vars = ComponentSet() + integer_vars = ComponentSet() + for v in m.vars: + if v.is_binary(): + binary_vars.add(v) + elif v.is_integer(): + integer_vars.add(v) + return list(binary_vars), list(integer_vars) + + +def relax_integers(binary_vars: Sequence[_GeneralVarData], integer_vars: Sequence[_GeneralVarData]): + for v in list(binary_vars) + list(integer_vars): + lb, ub = v.bounds + v.domain = pe.Reals + v.setlb(lb) + v.setub(ub) + + +def impose_structure(m): + m.aux_vars = pe.VarList() + + for key, c in list(m.nonlinear.cons.items()): + repn: StandardRepn = generate_standard_repn(c.body, quadratic=False, compute_values=True) + expr_list = split_expr(repn.nonlinear_expr) + if len(expr_list) == 1: + continue + + linear_coefs = list(repn.linear_coefs) + linear_vars = list(repn.linear_vars) + for term in expr_list: + v = m.aux_vars.add() + linear_coefs.append(1) + linear_vars.append(v) + m.vars.append(v) + if c.equality or (c.lb == c.ub and c.lb is not None): + m.nonlinear.cons.add(v == term) + elif c.ub is None: + m.nonlinear.cons.add(v <= term) + elif c.lb is None: + m.nonlinear.cons.add(v >= term) + else: + m.nonlinear.cons.add(v == term) + new_expr = LinearExpression(constant=repn.constant, linear_coefs=linear_coefs, linear_vars=linear_vars) + m.linear.cons.add((c.lb, new_expr, c.ub)) + del m.nonlinear.cons[key] + + if hasattr(m.nonlinear, 'obj'): + obj = m.nonlinear.obj + repn: StandardRepn = generate_standard_repn(obj.expr, quadratic=False, compute_values=True) + expr_list = split_expr(repn.nonlinear_expr) + if len(expr_list) > 1: + linear_coefs = list(repn.linear_coefs) + linear_vars = list(repn.linear_vars) + for term in expr_list: + v = m.aux_vars.add() + linear_coefs.append(1) + linear_vars.append(v) + m.vars.append(v) + if obj.sense == pe.minimize: + m.nonlinear.cons.add(v >= term) + else: + assert obj.sense == pe.maximize + m.nonlinear.cons.add(v <= term) + new_expr = LinearExpression(constant=repn.constant, linear_coefs=linear_coefs, linear_vars=linear_vars) + m.linear.obj = pe.Objective(expr=new_expr, sense=obj.sense) + del m.nonlinear.obj diff --git a/pyomo/contrib/coramin/algorithms/bnb/bnb.py b/pyomo/contrib/coramin/algorithms/bnb/bnb.py index 73d06bb3873..46c3b8e59a3 100644 --- a/pyomo/contrib/coramin/algorithms/bnb/bnb.py +++ b/pyomo/contrib/coramin/algorithms/bnb/bnb.py @@ -1,28 +1,20 @@ import pybnb from pyomo.common.timing import HierarchicalTimer from pyomo.core.base.block import _BlockData -from pyomo.common.modeling import unique_component_name import pyomo.environ as pe from pyomo.common.errors import InfeasibleConstraintException -from pyomo.core.expr.visitor import identify_variables from pyomo.common.collections import ComponentSet, ComponentMap from pyomo.contrib import appsi -from pyomo.common.config import ConfigDict, ConfigValue, PositiveFloat -from pyomo.contrib.coramin.clone import clone_active_flat +from pyomo.common.config import ConfigValue, PositiveFloat +from pyomo.contrib.coramin.clone import clone_shallow_active_flat, get_clone_and_var_map from pyomo.contrib.coramin.relaxations.auto_relax import _relax_cloned_model -from pyomo.repn.standard_repn import generate_standard_repn, StandardRepn -from pyomo.contrib.coramin.relaxations.split_expr import split_expr -from pyomo.core.expr.numeric_expr import LinearExpression from pyomo.contrib.coramin.relaxations import iterators from pyomo.contrib.coramin.utils.pyomo_utils import get_objective from pyomo.core.base.block import _BlockData -from pyomo.core.base.var import _GeneralVarData from pyomo.contrib.coramin.heuristics.diving import run_diving_heuristic from pyomo.contrib.coramin.domain_reduction.obbt import perform_obbt -from pyomo.contrib.coramin.cutting_planes.alpha_bb_cuts import AlphaBBCutGenerator from pyomo.contrib.coramin.cutting_planes.base import CutGenerator -from pyomo.contrib.coramin.utils.coramin_enums import EigenValueBounder -from typing import Tuple, List, Sequence, Optional +from typing import Tuple, List, Optional import math import numpy as np import logging @@ -35,43 +27,13 @@ SolverFactory, ) from pyomo.core.staleflag import StaleFlagManager +from pyomo.contrib.coramin.algorithms.alg_utils import impose_structure, collect_vars, relax_integers +from pyomo.contrib.coramin.algorithms.cut_gen import find_cut_generators, AlphaBBConfig logger = logging.getLogger(__name__) -def _get_clone_and_var_map(m1: _BlockData): - orig_vars = list() - for c in iterators.nonrelaxation_component_data_objects( - m1, pe.Constraint, active=True, descend_into=True - ): - for v in identify_variables(c.body, include_fixed=False): - orig_vars.append(v) - obj = get_objective(m1) - if obj is not None: - for v in identify_variables(obj.expr, include_fixed=False): - orig_vars.append(v) - for r in iterators.relaxation_data_objects(m1, descend_into=True, active=True): - orig_vars.extend(r.get_rhs_vars()) - orig_vars.append(r.get_aux_var()) - orig_vars = list(ComponentSet(orig_vars)) - tmp_name = unique_component_name(m1, "active_vars") - setattr(m1, tmp_name, orig_vars) - m2 = m1.clone() - new_vars = getattr(m2, tmp_name) - var_map = ComponentMap(zip(new_vars, orig_vars)) - delattr(m1, tmp_name) - delattr(m2, tmp_name) - return m2, var_map - - -class AlphaBBConfig(ConfigDict): - def __init__(self, description=None, doc=None, implicit=False, implicit_domain=None, visibility=0): - super().__init__(description, doc, implicit, implicit_domain, visibility) - self.max_num_vars: int = self.declare("max_num_vars", ConfigValue(default=4)) - self.method: EigenValueBounder = self.declare("method", ConfigValue(default=EigenValueBounder.GershgorinWithSimplification)) - - class BnBConfig(MIPSolverConfig): def __init__(self): super().__init__(None, None, False, None, 0) @@ -107,75 +69,6 @@ def __init__( self.active_cut_indices: List[int] = list() -def collect_vars(m: _BlockData) -> Tuple[List[_GeneralVarData], List[_GeneralVarData]]: - binary_vars = ComponentSet() - integer_vars = ComponentSet() - for v in m.vars: - if v.is_binary(): - binary_vars.add(v) - elif v.is_integer(): - integer_vars.add(v) - return list(binary_vars), list(integer_vars) - - -def relax_integers(binary_vars: Sequence[_GeneralVarData], integer_vars: Sequence[_GeneralVarData]): - for v in list(binary_vars) + list(integer_vars): - lb, ub = v.bounds - v.domain = pe.Reals - v.setlb(lb) - v.setub(ub) - - -def impose_structure(m): - m.aux_vars = pe.VarList() - - for key, c in list(m.nonlinear.cons.items()): - repn: StandardRepn = generate_standard_repn(c.body, quadratic=False, compute_values=True) - expr_list = split_expr(repn.nonlinear_expr) - if len(expr_list) == 1: - continue - - linear_coefs = list(repn.linear_coefs) - linear_vars = list(repn.linear_vars) - for term in expr_list: - v = m.aux_vars.add() - linear_coefs.append(1) - linear_vars.append(v) - m.vars.append(v) - if c.equality or (c.lb == c.ub and c.lb is not None): - m.nonlinear.cons.add(v == term) - elif c.ub is None: - m.nonlinear.cons.add(v <= term) - elif c.lb is None: - m.nonlinear.cons.add(v >= term) - else: - m.nonlinear.cons.add(v == term) - new_expr = LinearExpression(constant=repn.constant, linear_coefs=linear_coefs, linear_vars=linear_vars) - m.linear.cons.add((c.lb, new_expr, c.ub)) - del m.nonlinear.cons[key] - - if hasattr(m.nonlinear, 'obj'): - obj = m.nonlinear.obj - repn: StandardRepn = generate_standard_repn(obj.expr, quadratic=False, compute_values=True) - expr_list = split_expr(repn.nonlinear_expr) - if len(expr_list) > 1: - linear_coefs = list(repn.linear_coefs) - linear_vars = list(repn.linear_vars) - for term in expr_list: - v = m.aux_vars.add() - linear_coefs.append(1) - linear_vars.append(v) - m.vars.append(v) - if obj.sense == pe.minimize: - m.nonlinear.cons.add(v >= term) - else: - assert obj.sense == pe.maximize - m.nonlinear.cons.add(v <= term) - new_expr = LinearExpression(constant=repn.constant, linear_coefs=linear_coefs, linear_vars=linear_vars) - m.linear.obj = pe.Objective(expr=new_expr, sense=obj.sense) - del m.nonlinear.obj - - def _fix_vars_with_close_bounds(varlist, tol=1e-12): for v in varlist: if v.is_fixed(): @@ -188,35 +81,10 @@ def _fix_vars_with_close_bounds(varlist, tol=1e-12): v.fix(0.5 * (lb + ub)) -def find_cut_generators(m: _BlockData, config: AlphaBBConfig) -> List[CutGenerator]: - cut_generators = list() - for c in m.nonlinear.cons.values(): - repn: StandardRepn = generate_standard_repn(c.body, quadratic=False, compute_values=True) - if repn.nonlinear_expr is None: - continue - if len(repn.nonlinear_vars) > config.max_num_vars: - continue - - if len(repn.linear_coefs) > 0: - lhs = LinearExpression(constant=repn.constant, linear_coefs=repn.linear_coefs, linear_vars=repn.linear_vars) - else: - lhs = repn.constant - - # alpha bb convention is lhs >= rhs - if c.lb is not None: - cg = AlphaBBCutGenerator(lhs=lhs - c.lb, rhs=-repn.nonlinear_expr, eigenvalue_opt=None, method=config.method) - cut_generators.append(cg) - if c.ub is not None: - cg = AlphaBBCutGenerator(lhs=c.ub - lhs, rhs=repn.nonlinear_expr, eigenvalue_opt=None, method=config.method) - cut_generators.append(cg) - - return cut_generators - - class _BnB(pybnb.Problem): def __init__(self, model: _BlockData, config: BnBConfig, feasible_objective=None): # remove all parameters, fixed variables, etc. - nlp, relaxation = clone_active_flat(model, 2) + nlp, relaxation = clone_shallow_active_flat(model, 2) self.nlp: _BlockData = nlp self.relaxation: _BlockData = relaxation self.config = config @@ -538,20 +406,32 @@ def branch(self): for v, val in self.relaxation_solution.items(): v.set_value(val, skip_validation=True) - var_to_branch_on = None + int_var_to_branch_on = None max_viol = 0 for v in self.bin_and_int_vars: err = abs(v.value - round(v.value)) if err > max_viol and err > self.config.integer_tol: - var_to_branch_on = v + int_var_to_branch_on = v max_viol = err - if var_to_branch_on is None: - for r in self.relaxation_objects: - err = r.get_deviation() - if err > max_viol and err > self.config.feasibility_tol: - var_to_branch_on = r.get_rhs_vars()[0] - max_viol = err + max_viol = 0 + nl_var_to_branch_on = None + for r in self.relaxation_objects: + err = r.get_deviation() + if err > max_viol and err > self.config.feasibility_tol: + nl_var_to_branch_on = r.get_rhs_vars()[0] + max_viol = err + + if self.current_node.tree_depth % 2 == 0: + if int_var_to_branch_on is not None: + var_to_branch_on = int_var_to_branch_on + else: + var_to_branch_on = nl_var_to_branch_on + else: + if nl_var_to_branch_on is not None: + var_to_branch_on = nl_var_to_branch_on + else: + var_to_branch_on = int_var_to_branch_on if var_to_branch_on is None: # the relaxation was feasible @@ -587,7 +467,7 @@ def branch(self): def solve_with_bnb(model: _BlockData, config: BnBConfig): # we don't want to modify the original model - model, orig_var_map = _get_clone_and_var_map(model) + model, orig_var_map = get_clone_and_var_map(model) diving_obj, diving_sol = run_diving_heuristic(model, config.feasibility_tol, config.integer_tol) prob = _BnB(model, config, feasible_objective=diving_obj) res: pybnb.SolverResults = pybnb.solve( diff --git a/pyomo/contrib/coramin/algorithms/cut_gen.py b/pyomo/contrib/coramin/algorithms/cut_gen.py new file mode 100644 index 00000000000..78c58f57b54 --- /dev/null +++ b/pyomo/contrib/coramin/algorithms/cut_gen.py @@ -0,0 +1,41 @@ +from pyomo.core.base.block import _BlockData +from pyomo.repn.standard_repn import generate_standard_repn, StandardRepn +from pyomo.core.expr.numeric_expr import LinearExpression +from pyomo.core.base.block import _BlockData +from pyomo.contrib.coramin.cutting_planes.alpha_bb_cuts import AlphaBBCutGenerator +from pyomo.contrib.coramin.cutting_planes.base import CutGenerator +from pyomo.contrib.coramin.utils.coramin_enums import EigenValueBounder +from typing import List +from pyomo.common.config import ConfigDict, ConfigValue + + +class AlphaBBConfig(ConfigDict): + def __init__(self, description=None, doc=None, implicit=False, implicit_domain=None, visibility=0): + super().__init__(description, doc, implicit, implicit_domain, visibility) + self.max_num_vars: int = self.declare("max_num_vars", ConfigValue(default=4)) + self.method: EigenValueBounder = self.declare("method", ConfigValue(default=EigenValueBounder.GershgorinWithSimplification)) + + +def find_cut_generators(m: _BlockData, config: AlphaBBConfig) -> List[CutGenerator]: + cut_generators = list() + for c in m.nonlinear.cons.values(): + repn: StandardRepn = generate_standard_repn(c.body, quadratic=False, compute_values=True) + if repn.nonlinear_expr is None: + continue + if len(repn.nonlinear_vars) > config.max_num_vars: + continue + + if len(repn.linear_coefs) > 0: + lhs = LinearExpression(constant=repn.constant, linear_coefs=repn.linear_coefs, linear_vars=repn.linear_vars) + else: + lhs = repn.constant + + # alpha bb convention is lhs >= rhs + if c.lb is not None: + cg = AlphaBBCutGenerator(lhs=lhs - c.lb, rhs=-repn.nonlinear_expr, eigenvalue_opt=None, method=config.method) + cut_generators.append(cg) + if c.ub is not None: + cg = AlphaBBCutGenerator(lhs=c.ub - lhs, rhs=repn.nonlinear_expr, eigenvalue_opt=None, method=config.method) + cut_generators.append(cg) + + return cut_generators diff --git a/pyomo/contrib/coramin/algorithms/multitree/multitree.py b/pyomo/contrib/coramin/algorithms/multitree/multitree.py index 52f542aa9d7..f1bf8a666ce 100644 --- a/pyomo/contrib/coramin/algorithms/multitree/multitree.py +++ b/pyomo/contrib/coramin/algorithms/multitree/multitree.py @@ -26,18 +26,13 @@ InEnum, ) import logging -from pyomo.contrib.coramin.relaxations.auto_relax import relax +from pyomo.contrib.coramin.relaxations.auto_relax import _relax_cloned_model from pyomo.contrib.coramin.relaxations.iterators import relaxation_data_objects from pyomo.contrib.coramin.utils.coramin_enums import ( RelaxationSide, Effort, EigenValueBounder, ) -from pyomo.contrib.coramin.domain_reduction.dbt import ( - push_integers, - pop_integers, - collect_vars_to_tighten, -) from pyomo.contrib.coramin.domain_reduction.obbt import perform_obbt import time from pyomo.core.base.var import _GeneralVarData @@ -49,7 +44,9 @@ from pyomo.contrib.fbbt.fbbt import BoundsManager import numpy as np from pyomo.core.expr.visitor import identify_variables -from pyomo.contrib.coramin.clone import clone_active_flat +from pyomo.contrib.coramin.clone import clone_shallow_active_flat, get_clone_and_var_map +from pyomo.contrib.coramin.algorithms.cut_gen import AlphaBBConfig, find_cut_generators +from pyomo.contrib.coramin.algorithms.alg_utils import impose_structure, collect_vars, relax_integers logger = logging.getLogger(__name__) @@ -161,10 +158,9 @@ def __init__(self, mip_solver: PersistentSolver, nlp_solver: PersistentSolver): self._relaxation_objects: Optional[Sequence[BaseRelaxationData]] = None self._stop: Optional[TerminationCondition] = None self._discrete_vars: Optional[Sequence[_GeneralVarData]] = None - self._rel_to_nlp_map: Optional[MutableMapping] = None - self._nlp_to_orig_map: Optional[MutableMapping] = None self._nlp_tightener: Optional[appsi.fbbt.IntervalTightener] = None self._iter: int = 0 + self._cut_generators = None def _re_init(self): self._original_model: Optional[_BlockData] = None @@ -414,24 +410,6 @@ def _update_dual_bound(self, res: Results): all_cons_satisfied = False break if all_cons_satisfied: - for rel_v, nlp_v in self._rel_to_nlp_map.items(): - if rel_v.value is None: - assert rel_v.stale - if ( - rel_v.has_lb() - and rel_v.has_ub() - and math.isclose( - rel_v.lb, - rel_v.ub, - rel_tol=self.config.feasibility_tolerance, - abs_tol=self.config.feasibility_tolerance, - ) - ): - nlp_v.value = 0.5 * (rel_v.lb + rel_v.ub) - else: - nlp_v.value = None - else: - nlp_v.set_value(rel_v.value, skip_validation=True) self._update_primal_bound(res) def _update_primal_bound(self, res: Results): @@ -453,7 +431,7 @@ def _update_primal_bound(self, res: Results): if should_update: self._best_feasible_objective = res.best_feasible_objective self._incumbent = pe.ComponentMap() - for nlp_v, orig_v in self._nlp_to_orig_map.items(): + for nlp_v, orig_v in self._orig_var_map.items(): self._incumbent[orig_v] = nlp_v.value def _solve_nlp_with_fixed_vars( @@ -463,8 +441,9 @@ def _solve_nlp_with_fixed_vars( ) -> Results: self._iter += 1 - bm = BoundsManager(self._nlp) - bm.save_bounds() + orig_bounds = pe.ComponentMap() + for v in self._nlp.vars: + orig_bounds[v] = v.bounds fixed_vars = list() for v in self._discrete_vars: @@ -478,19 +457,14 @@ def _solve_nlp_with_fixed_vars( abs_tol=self.config.integer_tolerance, ) val = round(val) - nlp_v = self._rel_to_nlp_map[v] - orig_v = self._nlp_to_orig_map[nlp_v] - nlp_v.fix(val) - orig_v.fix(val) - fixed_vars.append(nlp_v) - fixed_vars.append(orig_v) + v.fix(val) + fixed_vars.append(v) for v, (v_lb, v_ub) in rhs_var_bounds.items(): if v.fixed: continue - nlp_v = self._rel_to_nlp_map[v] - nlp_v.setlb(v_lb) - nlp_v.setub(v_ub) + v.setlb(v_lb) + v.setub(v_ub) nlp_res = Results() @@ -511,9 +485,7 @@ def _solve_nlp_with_fixed_vars( if proven_infeasible: any_unfixed_vars = False - for v in self._original_model.component_data_objects( - pe.Var, descend_into=True - ): + for v in self._original_model.vars: if not v.fixed: any_unfixed_vars = True break @@ -522,15 +494,11 @@ def _solve_nlp_with_fixed_vars( nlp_res = self.nlp_solver.solve(self._original_model) if nlp_res.best_feasible_objective is not None: nlp_res.solution_loader.load_vars() - for nlp_v, orig_v in self._nlp_to_orig_map.items(): - nlp_v.set_value(orig_v.value, skip_validation=True) else: nlp_res = Results() nlp_res.termination_condition = TerminationCondition.infeasible else: - for v in ComponentSet( - self._nlp.component_data_objects(pe.Var, descend_into=True) - ): + for v in self._nlp.vars: if v.fixed: continue if v.has_lb() and v.has_ub(): @@ -579,8 +547,6 @@ def _solve_nlp_with_fixed_vars( solve_error = True if not solve_error and nlp_res.best_feasible_objective is not None: nlp_res.solution_loader.load_vars() - for nlp_v, orig_v in self._nlp_to_orig_map.items(): - nlp_v.value = orig_v.value else: nlp_obj = get_objective(self._nlp) # there should not be any active constraints @@ -592,10 +558,7 @@ def _solve_nlp_with_fixed_vars( nlp_res.best_objective_bound = nlp_res.best_feasible_objective nlp_res.solution_loader = MultiTreeSolutionLoader( pe.ComponentMap( - (v, v.value) - for v in self._nlp.component_data_objects( - pe.Var, descend_into=True - ) + (v, v.value) for v in self._nlp.vars ) ) @@ -605,7 +568,9 @@ def _solve_nlp_with_fixed_vars( for v in fixed_vars: v.unfix() - bm.pop_bounds() + for v, (lb, ub) in orig_bounds.items(): + v.setlb(lb) + v.setub(ub) for c in active_constraints: c.activate() @@ -689,6 +654,37 @@ def _oa_cut_helper(self, tol): self.mip_solver.add_constraints(new_con_list) return new_con_list + def _run_cut_generators(self, max_iter): + last_res = None + + for _iter in range(max_iter): + added_cuts = False + for cg in self._cut_generators: + cut_expr = cg.generate(None) + if cut_expr is not None: + self._relaxation.cuts.add(cut_expr) + if added_cuts: + if self._should_terminate()[0]: + break + rel_res = self._solve_relaxation() + last_res = Results() + last_res.best_feasible_objective = rel_res.best_feasible_objective + last_res.best_objective_bound = rel_res.best_objective_bound + last_res.termination_condition = rel_res.termination_condition + last_res.solution_loader = MultiTreeSolutionLoader( + rel_res.solution_loader.get_primals( + vars_to_load=self._discrete_vars + ) + ) + else: + break + + if last_res is None: + last_res = Results() + + return last_res + + def _add_oa_cuts(self, tol, max_iter) -> Results: original_update_config: UpdateConfig = self.mip_solver.update_config() @@ -765,86 +761,14 @@ def _add_oa_cuts(self, tol, max_iter) -> Results: return last_res def _construct_nlp(self): - all_vars = list( - ComponentSet( - self._original_model.component_data_objects(pe.Var, descend_into=True) - ) - ) - tmp_name = unique_component_name(self._original_model, "all_vars") - setattr(self._original_model, tmp_name, all_vars) - - # this has to be 0 because the Multitree solver cannot use alpha-bb relaxations - max_vars_per_alpha_bb = 0 - max_eigenvalue_for_alpha_bb = 0 - - if self.config.convexity_effort == Effort.none: - perform_expression_simplification = False - use_alpha_bb = False - eigenvalue_bounder = EigenValueBounder.Gershgorin - eigenvalue_opt = None - elif self.config.convexity_effort <= Effort.low: - perform_expression_simplification = False - use_alpha_bb = True - eigenvalue_bounder = EigenValueBounder.Gershgorin - eigenvalue_opt = None - elif self.config.convexity_effort <= Effort.medium: - perform_expression_simplification = True - use_alpha_bb = True - eigenvalue_bounder = EigenValueBounder.GershgorinWithSimplification - eigenvalue_opt = None - elif self.config.convexity_effort <= Effort.high: - perform_expression_simplification = True - use_alpha_bb = True - eigenvalue_bounder = EigenValueBounder.LinearProgram - eigenvalue_opt = self.mip_solver.__class__() - eigenvalue_opt.config = self.mip_solver.config() - # TODO: need to update the solver options - else: - perform_expression_simplification = True - use_alpha_bb = True - eigenvalue_bounder = EigenValueBounder.Global - mip_solver = self.mip_solver.__class__() - mip_solver.config = self.mip_solver.config() - nlp_solver = self.nlp_solver.__class__() - nlp_solver.config = self.nlp_solver.config() - eigenvalue_opt = MultiTree(mip_solver=mip_solver, nlp_solver=nlp_solver) - eigenvalue_opt.config = self.config() - eigenvalue_opt.config.convexity_effort = min( - self.config.convexity_effort, Effort.medium - ) - - self._nlp = relax( - model=self._original_model, - in_place=False, - use_fbbt=True, - fbbt_options={"deactivate_satisfied_constraints": True, "max_iter": 5}, - perform_expression_simplification=perform_expression_simplification, - use_alpha_bb=use_alpha_bb, - eigenvalue_bounder=eigenvalue_bounder, - eigenvalue_opt=eigenvalue_opt, - max_vars_per_alpha_bb=max_vars_per_alpha_bb, - max_eigenvalue_for_alpha_bb=max_eigenvalue_for_alpha_bb, - ) - new_vars = getattr(self._nlp, tmp_name) - self._nlp_to_orig_map = pe.ComponentMap(zip(new_vars, all_vars)) - delattr(self._original_model, tmp_name) - delattr(self._nlp, tmp_name) - + self._nlp = clone_shallow_active_flat(self._relaxation)[0] for b in relaxation_data_objects(self._nlp, descend_into=True, active=True): b.rebuild(build_nonlinear_constraint=True) def _construct_relaxation(self): - all_vars = list( - ComponentSet(self._nlp.component_data_objects(pe.Var, descend_into=True)) - ) - tmp_name = unique_component_name(self._nlp, "all_vars") - setattr(self._nlp, tmp_name, all_vars) - self._relaxation = self._nlp.clone() - new_vars = getattr(self._relaxation, tmp_name) - self._rel_to_nlp_map = pe.ComponentMap(zip(new_vars, all_vars)) - delattr(self._nlp, tmp_name) - delattr(self._relaxation, tmp_name) - + _relax_cloned_model(self._relaxation) + self._relaxation.cuts = pe.ConstraintList() + self._relaxation_objects = list() for b in relaxation_data_objects( self._relaxation, descend_into=True, active=True ): @@ -852,6 +776,7 @@ def _construct_relaxation(self): b.large_coef = self.config.large_coef b.safety_tol = self.config.safety_tol b.rebuild() + self._relaxation_objects.append(b) def _get_nlp_specs_from_rel(self): integer_var_values = pe.ComponentMap() @@ -960,7 +885,6 @@ def _perform_obbt(self, vars_to_tighten): def solve( self, model: _BlockData, timer: HierarchicalTimer = None ) -> MultiTreeResults: - model = clone_active_flat(model) self._re_init() self._start_time = time.time() @@ -968,21 +892,26 @@ def solve( timer = HierarchicalTimer() timer.start("solve") - self._original_model = model + model, self._orig_var_map = get_clone_and_var_map(model) + + self._original_model, self._relaxation = clone_shallow_active_flat(model, 2) + model = self._original_model self._log(header=True) + it = appsi.fbbt.IntervalTightener() + it.config.deactivate_satisfied_constraints = True + it.perform_fbbt(self._relaxation) + timer.start("construct relaxation") - self._construct_nlp() + impose_structure(self._relaxation) + self._cut_generators = find_cut_generators(self._relaxation, AlphaBBConfig()) self._construct_relaxation() + self._construct_nlp() + it.perform_fbbt(self._nlp) timer.stop("construct relaxation") self._objective = get_objective(self._relaxation) - self._relaxation_objects = list() - for r in relaxation_data_objects( - self._relaxation, descend_into=True, active=True - ): - self._relaxation_objects.append(r) should_terminate, reason = self._should_terminate() if should_terminate: @@ -996,10 +925,15 @@ def solve( self._nlp_tightener.config.feasibility_tol = self.config.feasibility_tolerance self._nlp_tightener.set_instance(self._nlp, symbolic_solver_labels=False) - relaxed_binaries, relaxed_integers = push_integers(self._relaxation) - self._discrete_vars = list(relaxed_binaries) + list(relaxed_integers) + bin_vars, int_vars = collect_vars(self._relaxation) + relax_integers(bin_vars, int_vars) + self._discrete_vars = list(bin_vars) + list(int_vars) oa_results = self._add_oa_cuts(self.config.feasibility_tolerance * 100, 100) - pop_integers(relaxed_binaries, relaxed_integers) + oa_results = self._run_cut_generators(100) + for v in bin_vars: + v.domain = pe.Binary + for v in int_vars: + v.domain = pe.Integers should_terminate, reason = self._should_terminate() if should_terminate: @@ -1020,14 +954,21 @@ def solve( integer_var_values, rhs_var_bounds ) - vars_to_tighten = collect_vars_to_tighten(self._relaxation) + vars_to_tighten = ComponentSet() + for r in relaxation_data_objects(self._relaxation, descend_into=True, active=True): + vars_to_tighten.update(r.get_rhs_vars()) + vars_to_tighten = list(vars_to_tighten) for obbt_iter in range(self.config.root_obbt_max_iter): should_terminate, reason = self._should_terminate() if should_terminate: return self._get_results(reason) - relaxed_binaries, relaxed_integers = push_integers(self._relaxation) + relax_integers(bin_vars, int_vars) num_lb, num_ub, avg_lb, avg_ub = self._perform_obbt(vars_to_tighten) - pop_integers(relaxed_binaries, relaxed_integers) + it.perform_fbbt(self._nlp) + for v in bin_vars: + v.domain = pe.Binary + for v in int_vars: + v.domain = pe.Integers should_terminate, reason = self._should_terminate() if (num_lb + num_ub) < 1 or (avg_lb < 1e-3 and avg_ub < 1e-3): break @@ -1065,12 +1006,14 @@ def solve( start_primal_bound, end_primal_bound, rel_tol=1e-4, abs_tol=1e-4 ): if self.config.relax_integers_for_obbt: - relaxed_binaries, relaxed_integers = push_integers( - self._relaxation - ) + relax_integers(bin_vars, int_vars) num_lb, num_ub, avg_lb, avg_ub = self._perform_obbt(vars_to_tighten) + it.perform_fbbt(self._nlp) if self.config.relax_integers_for_obbt: - pop_integers(relaxed_binaries, relaxed_integers) + for v in bin_vars: + v.domain = pe.Binary + for v in int_vars: + v.domain = pe.Integers else: self.config.solver_output_logger.warning( f"relaxation did not find a feasible solution: " diff --git a/pyomo/contrib/coramin/clone.py b/pyomo/contrib/coramin/clone.py index f036116d339..bf21e05a71f 100644 --- a/pyomo/contrib/coramin/clone.py +++ b/pyomo/contrib/coramin/clone.py @@ -6,9 +6,36 @@ from pyomo.common.collections import ComponentSet, ComponentMap from pyomo.core.base.block import _BlockData from typing import List +from pyomo.core.expr.visitor import identify_variables +from pyomo.common.modeling import unique_component_name -def clone_active_flat(m1: _BlockData, num_clones: int = 1) -> List[_BlockData]: +def get_clone_and_var_map(m1: _BlockData): + orig_vars = list() + for c in iterators.nonrelaxation_component_data_objects( + m1, pe.Constraint, active=True, descend_into=True + ): + for v in identify_variables(c.body, include_fixed=False): + orig_vars.append(v) + obj = get_objective(m1) + if obj is not None: + for v in identify_variables(obj.expr, include_fixed=False): + orig_vars.append(v) + for r in iterators.relaxation_data_objects(m1, descend_into=True, active=True): + orig_vars.extend(r.get_rhs_vars()) + orig_vars.append(r.get_aux_var()) + orig_vars = list(ComponentSet(orig_vars)) + tmp_name = unique_component_name(m1, "active_vars") + setattr(m1, tmp_name, orig_vars) + m2 = m1.clone() + new_vars = getattr(m2, tmp_name) + var_map = ComponentMap(zip(new_vars, orig_vars)) + delattr(m1, tmp_name) + delattr(m2, tmp_name) + return m2, var_map + + +def clone_shallow_active_flat(m1: _BlockData, num_clones: int = 1) -> List[_BlockData]: clone_list = [pe.Block(concrete=True) for i in range(num_clones)] for m2 in clone_list: m2.linear = pe.Block() diff --git a/pyomo/contrib/coramin/cutting_planes/alpha_bb_cuts.py b/pyomo/contrib/coramin/cutting_planes/alpha_bb_cuts.py index 6bf9fac024d..b522b352556 100644 --- a/pyomo/contrib/coramin/cutting_planes/alpha_bb_cuts.py +++ b/pyomo/contrib/coramin/cutting_planes/alpha_bb_cuts.py @@ -41,22 +41,33 @@ def _most_recent_ancestor(self, node: pybnb.Node): node = p return res - def generate(self, node: pybnb.Node) -> Optional[RelationalExpression]: - lhs_val = value(self.lhs) + def generate(self, node: Optional[pybnb.Node]) -> Optional[RelationalExpression]: + try: + lhs_val = value(self.lhs) + except ValueError: + return None if lhs_val + self.feasibility_tol >= value(self.rhs): return None - - mra = self._most_recent_ancestor(node) - if mra is not None and self._proven_convex[mra]: + + if node is None: + mra = None + else: + mra = self._most_recent_ancestor(node) + if mra in self._proven_convex and self._proven_convex[mra]: alpha_bb_rhs = self.rhs else: alpha = max(0, -0.5 * self.hessian.get_minimum_eigenvalue()) - alpha_sum = 0 - for ndx, v in enumerate(self.xlist): - lb, ub = v.bounds - alpha_sum += (v - lb) * (v - ub) - alpha_bb_rhs = self.rhs + alpha * alpha_sum - if lhs_val + self.feasibility_tol >= value(alpha_bb_rhs): - return None + if alpha == 0: + self._proven_convex[node] = True + alpha_bb_rhs = self.rhs + else: + self._proven_convex[node] = False + alpha_sum = 0 + for ndx, v in enumerate(self.xlist): + lb, ub = v.bounds + alpha_sum += (v - lb) * (v - ub) + alpha_bb_rhs = self.rhs + alpha * alpha_sum + if lhs_val + self.feasibility_tol >= value(alpha_bb_rhs): + return None return self.lhs >= taylor_series_expansion(alpha_bb_rhs) diff --git a/pyomo/contrib/coramin/cutting_planes/base.py b/pyomo/contrib/coramin/cutting_planes/base.py index bf8c95d6e87..1e9536c2c38 100644 --- a/pyomo/contrib/coramin/cutting_planes/base.py +++ b/pyomo/contrib/coramin/cutting_planes/base.py @@ -7,5 +7,5 @@ class CutGenerator(ABC): @abstractmethod - def generate(self, node: pybnb.Node): + def generate(self, node: Optional[pybnb.Node]): pass diff --git a/pyomo/contrib/coramin/heuristics/diving.py b/pyomo/contrib/coramin/heuristics/diving.py index 6109ce5ed99..ca8aea52ab5 100644 --- a/pyomo/contrib/coramin/heuristics/diving.py +++ b/pyomo/contrib/coramin/heuristics/diving.py @@ -12,7 +12,7 @@ from pyomo.contrib.appsi.fbbt import IntervalTightener, InfeasibleConstraintException from typing import Sequence from .binary_multiplication_reformulation import reformulate_binary_multiplication -from pyomo.contrib.coramin.clone import clone_active_flat +from pyomo.contrib.coramin.clone import clone_shallow_active_flat from pyomo.contrib.coramin.relaxations import iterators @@ -58,7 +58,7 @@ def __init__(self, m: _BlockData) -> None: super().__init__() binary_vars, integer_vars, all_vars = collect_vars(m) - self.relaxation = clone_active_flat(reformulate_binary_multiplication(m))[0] + self.relaxation = clone_shallow_active_flat(reformulate_binary_multiplication(m))[0] orig_lbs = [v.lb for v in self.relaxation.vars] orig_ubs = [v.ub for v in self.relaxation.vars] diff --git a/pyomo/contrib/coramin/relaxations/auto_relax.py b/pyomo/contrib/coramin/relaxations/auto_relax.py index 53510a2272b..b30114f5460 100644 --- a/pyomo/contrib/coramin/relaxations/auto_relax.py +++ b/pyomo/contrib/coramin/relaxations/auto_relax.py @@ -26,7 +26,7 @@ from pyomo.core.base.expression import _GeneralExpressionData, SimpleExpression from pyomo.repn.standard_repn import generate_standard_repn from .iterators import relaxation_data_objects -from pyomo.contrib.coramin.clone import clone_active_flat +from pyomo.contrib.coramin.clone import clone_shallow_active_flat logger = logging.getLogger(__name__) @@ -1332,6 +1332,6 @@ def relax( m: pyomo.core.base.block._BlockData or pyomo.core.base.PyomoModel.ConcreteModel The relaxed model """ - m = clone_active_flat(model)[0] + m = clone_shallow_active_flat(model)[0] _relax_cloned_model(m) return m From f55fb1ac2366d9b8b73332c323ecd8b6bf048f3d Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Fri, 26 Jan 2024 07:42:48 -0700 Subject: [PATCH 084/128] coramin: update tests --- pyomo/contrib/coramin/algorithms/bnb/bnb.py | 2 +- .../coramin/algorithms/multitree/multitree.py | 28 ++++++++++++----- .../multitree/tests/test_multitree.py | 30 ++++++++++++++----- .../algorithms/tests/test_ecp_bounder.py | 2 +- 4 files changed, 44 insertions(+), 18 deletions(-) diff --git a/pyomo/contrib/coramin/algorithms/bnb/bnb.py b/pyomo/contrib/coramin/algorithms/bnb/bnb.py index 46c3b8e59a3..e771152f1aa 100644 --- a/pyomo/contrib/coramin/algorithms/bnb/bnb.py +++ b/pyomo/contrib/coramin/algorithms/bnb/bnb.py @@ -38,7 +38,7 @@ class BnBConfig(MIPSolverConfig): def __init__(self): super().__init__(None, None, False, None, 0) self.feasibility_tol = self.declare( - "feasibility_tol", ConfigValue(domain=PositiveFloat, default=1e-6) + "feasibility_tol", ConfigValue(domain=PositiveFloat, default=1e-7) ) self.lp_solver = self.declare("lp_solver", ConfigValue()) self.nlp_solver = self.declare("nlp_solver", ConfigValue()) diff --git a/pyomo/contrib/coramin/algorithms/multitree/multitree.py b/pyomo/contrib/coramin/algorithms/multitree/multitree.py index f1bf8a666ce..6f1d4da6a36 100644 --- a/pyomo/contrib/coramin/algorithms/multitree/multitree.py +++ b/pyomo/contrib/coramin/algorithms/multitree/multitree.py @@ -87,7 +87,7 @@ def __init__( self.solver_output_logger = logger self.log_level = logging.INFO - self.feasibility_tolerance = 1e-6 + self.feasibility_tolerance = 1e-7 self.integer_tolerance = 1e-4 self.time_limit = 600 self.abs_gap = 1e-4 @@ -141,6 +141,18 @@ def get_primals( return primals +def _fix_vars_with_close_bounds(varlist, tol=1e-12): + for v in varlist: + if v.is_fixed(): + v.setlb(v.value) + v.setub(v.value) + lb, ub = v.bounds + if lb is None or ub is None: + continue + if abs(ub - lb) <= tol * min(abs(lb), abs(ub)) + tol: + v.fix(0.5 * (lb + ub)) + + class MultiTree(Solver): def __init__(self, mip_solver: PersistentSolver, nlp_solver: PersistentSolver): super(MultiTree, self).__init__() @@ -217,7 +229,7 @@ def _should_terminate(self) -> Tuple[bool, Optional[TerminationCondition]]: if self._objective.sense == pe.minimize: assert ( primal_bound - >= dual_bound - 1e-6 * max(abs(primal_bound), abs(dual_bound)) - 1e-6 + >= dual_bound - 1e-4 * max(abs(primal_bound), abs(dual_bound)) - 1e-4 ) else: assert ( @@ -684,7 +696,6 @@ def _run_cut_generators(self, max_iter): return last_res - def _add_oa_cuts(self, tol, max_iter) -> Results: original_update_config: UpdateConfig = self.mip_solver.update_config() @@ -899,18 +910,19 @@ def solve( self._log(header=True) - it = appsi.fbbt.IntervalTightener() - it.config.deactivate_satisfied_constraints = True - it.perform_fbbt(self._relaxation) - timer.start("construct relaxation") impose_structure(self._relaxation) self._cut_generators = find_cut_generators(self._relaxation, AlphaBBConfig()) self._construct_relaxation() self._construct_nlp() - it.perform_fbbt(self._nlp) timer.stop("construct relaxation") + it = appsi.fbbt.IntervalTightener() + it.config.deactivate_satisfied_constraints = True + it.perform_fbbt(self._relaxation) + it.perform_fbbt(self._nlp) + _fix_vars_with_close_bounds(self._nlp.vars) + self._objective = get_objective(self._relaxation) should_terminate, reason = self._should_terminate() diff --git a/pyomo/contrib/coramin/algorithms/multitree/tests/test_multitree.py b/pyomo/contrib/coramin/algorithms/multitree/tests/test_multitree.py index 8fc1fd9e501..6dcdf87c2c8 100644 --- a/pyomo/contrib/coramin/algorithms/multitree/tests/test_multitree.py +++ b/pyomo/contrib/coramin/algorithms/multitree/tests/test_multitree.py @@ -17,6 +17,7 @@ def _get_sol(pname): + start_x1_set = {'batch0812', 'chem'} current_dir = os.getcwd() target_fname = os.path.join(current_dir, f'{pname}.sol') downloader = download.FileDownloader() @@ -28,8 +29,20 @@ def _get_sol(pname): l = line.split() vname = l[0] vval = float(l[1]) - if vname != 'objvar': - res[vname] = vval + if vname == 'objvar': + continue + assert vname.startswith('x') or vname.startswith('b') + if vname.startswith('x'): + ndx = int(vname.replace('x', '')) - 1 + if pname in start_x1_set: + ndx += 1 + vname = f'x{ndx}' + else: + ndx = int(vname.replace('b', '')) - 1 + if pname in start_x1_set: + ndx += 1 + vname = f'b{ndx}' + res[vname] = vval f.close() return res @@ -52,7 +65,7 @@ class TestMultiTreeWithMINLPLib(Helper): @classmethod def setUpClass(self) -> None: self.test_problems = { - 'batch': 285506.5082, + 'batch0812': 2687026.784, 'ball_mk3_10': None, 'ball_mk2_10': 0, 'syn05m': 837.73240090, @@ -61,7 +74,7 @@ def setUpClass(self) -> None: 'alkyl': -1.76499965, } self.primal_sol = dict() - self.primal_sol['batch'] = _get_sol('batch') + self.primal_sol['batch0812'] = _get_sol('batch0812') self.primal_sol['alkyl'] = _get_sol('alkyl') self.primal_sol['ball_mk2_10'] = _get_sol('ball_mk2_10') self.primal_sol['syn05m'] = _get_sol('syn05m') @@ -148,8 +161,8 @@ def time_limit_helper(self, pname): ) self.opt.config.load_solution = True - def test_batch(self): - self.optimal_helper('batch') + def test_batch0812(self): + self.optimal_helper('batch0812') def test_ball_mk2_10(self): self.optimal_helper('ball_mk2_10') @@ -171,7 +184,7 @@ def test_chem(self): self.opt.config = orig_config def test_time_limit(self): - self.time_limit_helper('alkyl') + self.time_limit_helper('chem') def test_ball_mk3_10(self): self.infeasible_helper('ball_mk3_10') @@ -223,12 +236,13 @@ def test_max_iter(self): opt = coramin.algorithms.MultiTree(mip_solver=mip_solver, nlp_solver=nlp_solver) opt.config.max_iter = 3 opt.config.load_solution = False + opt.config.stream_solver = True res = opt.solve(m) self.assertEqual( res.termination_condition, appsi.base.TerminationCondition.maxIterations ) self.assertIsNone(res.best_feasible_objective) - opt.config.max_iter = 10 + opt.config.max_iter = 12 res = opt.solve(m) self.assertEqual( res.termination_condition, appsi.base.TerminationCondition.maxIterations diff --git a/pyomo/contrib/coramin/algorithms/tests/test_ecp_bounder.py b/pyomo/contrib/coramin/algorithms/tests/test_ecp_bounder.py index 538c58c7101..061b9eb6b62 100644 --- a/pyomo/contrib/coramin/algorithms/tests/test_ecp_bounder.py +++ b/pyomo/contrib/coramin/algorithms/tests/test_ecp_bounder.py @@ -17,7 +17,7 @@ def test_ecp_bounder(self): m.obj = pe.Objective(expr=0.5 * (m.x**2 + m.y**2)) m.c1 = pe.Constraint(expr=m.y >= (m.x - 1) ** 2) m.c2 = pe.Constraint(expr=m.y >= pe.exp(m.x)) - coramin.relaxations.relax(m, in_place=True) + coramin.relaxations.relax(m) opt = ECPBounder(subproblem_solver=appsi.solvers.Gurobi()) res = opt.solve(m) self.assertEqual( From ee36bd877fa463706a3ee02f518635f612ff1429 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 30 Jan 2024 00:11:08 -0700 Subject: [PATCH 085/128] coramin: clean up domain reduction --- pyomo/contrib/coramin/domain_reduction/dbt.py | 426 ++++-------------- .../coramin/domain_reduction/filters.py | 3 +- .../contrib/coramin/domain_reduction/obbt.py | 19 +- .../coramin/relaxations/copy_relaxation.py | 9 +- 4 files changed, 108 insertions(+), 349 deletions(-) diff --git a/pyomo/contrib/coramin/domain_reduction/dbt.py b/pyomo/contrib/coramin/domain_reduction/dbt.py index 844b99a3ded..b41ae353c7d 100644 --- a/pyomo/contrib/coramin/domain_reduction/dbt.py +++ b/pyomo/contrib/coramin/domain_reduction/dbt.py @@ -1,5 +1,5 @@ import networkx as nx -from typing import Sequence, MutableSet, Optional +from typing import Sequence, MutableSet, Optional, Union from pyomo.contrib.fbbt.fbbt import fbbt, compute_bounds_on_expr import time import enum @@ -18,6 +18,8 @@ from pyomo.core.expr.visitor import replace_expressions import logging import networkx +from pyomo.contrib.coramin.clone import clone_shallow_active_flat +from pyomo.repn.standard_repn import generate_standard_repn try: import metis @@ -59,13 +61,12 @@ def __init__(self, component): _BlockData.__init__(self, component) self._children_index = None self._children = None - self._linking_constraints = None + self._coupling_vars = list() self._already_setup = False self._is_leaf = None self._is_root = True - self._allow_changes = False - def setup(self, children_keys): + def setup(self, children_keys, coupling_vars): assert not self._already_setup self._already_setup = True if len(children_keys) == 0: @@ -74,12 +75,10 @@ def setup(self, children_keys): self._is_leaf = False del self._children_index del self._children - del self._linking_constraints - self._allow_changes = True + del self._coupling_vars self._children_index = pe.Set(initialize=children_keys) self._children = TreeBlock(self._children_index) - self._linking_constraints = pe.ConstraintList() - self._allow_changes = False + self._coupling_vars = list(coupling_vars) for key in children_keys: child = self.children[key] child._is_root = False @@ -94,19 +93,6 @@ def is_leaf(self): self._assert_setup() return self._is_leaf - def add_component(self, name, val): - self._assert_setup() - if ( - self.is_leaf() - or self._allow_changes - or not isinstance(val, _GeneralVarData) - ): - _BlockData.add_component(self, name, val) - else: - raise TreeBlockError( - 'Pyomo variables cannot be added to a TreeBlock unless it is a leaf.' - ) - @property def children(self): self._assert_setup() @@ -115,15 +101,11 @@ def children(self): 'Leaf TreeBlocks do not have children. Please check the is_leaf method' ) return self._children - + @property - def linking_constraints(self): + def coupling_vars(self): self._assert_setup() - if self.is_leaf(): - raise TreeBlockError( - 'leaf TreeBlocks do not have linking_constraints. Please check the is_leaf method.' - ) - return self._linking_constraints + return list(self._coupling_vars) def _num_stages(self): self._assert_setup() @@ -234,19 +216,19 @@ def __str__(self): class _Tree(object): - def __init__(self, children=None, edges_between_children=None): + def __init__(self, children, coupling_vars): """ Parameters ---------- - children: list or collections.abc.Iterable of _Tree or networkx.Graph - edges_between_children: list or collections.abc.Iterable of _Edge + children: Sequence[networkx.Graph] + coupling_vars: Sequence[_VarNode] """ - self.children = OrderedSet() - self.edges_between_children = OrderedSet() + self.children: MutableSet[Union[_Tree, networkx.Graph]] = OrderedSet() + self.coupling_vars: MutableSet[_VarNode] = OrderedSet() if children is not None: self.children.update(children) - if edges_between_children is not None: - self.edges_between_children.update(edges_between_children) + if coupling_vars is not None: + self.coupling_vars.update(coupling_vars) def build_pyomo_model(self, block): """ @@ -254,56 +236,20 @@ def build_pyomo_model(self, block): ---------- block: TreeBlockData empty TreeBlock - - Returns - ------- - component_map: pe.ComponentMap """ - block.setup(children_keys=list(range(len(self.children)))) - component_map = pe.ComponentMap() - replacement_map_by_child = dict() + block.setup(children_keys=list(range(len(self.children))), coupling_vars=[i.comp for i in self.coupling_vars]) for i, child in enumerate(self.children): if isinstance(child, _Tree): - tmp_component_map = child.build_pyomo_model(block=block.children[i]) + child.build_pyomo_model(block=block.children[i]) elif isinstance(child, networkx.Graph): - block.children[i].setup(children_keys=list()) - tmp_component_map = build_pyomo_model_from_graph( - graph=child, block=block.children[i] - ) + block.children[i].setup(children_keys=list(), coupling_vars=list()) + build_pyomo_model_from_graph(graph=child, block=block.children[i]) else: raise ValueError('Unexpected child type: {0}'.format(str(type(child)))) - replacement_map_by_child[child] = tmp_component_map - component_map.update(tmp_component_map) - - logger.debug( - 'creating linking cons linking the children of {0}'.format(str(block)) - ) - for edge in self.edges_between_children: - logger.debug('adding linking constraint for edge {0}'.format(str(edge))) - if edge.node1.comp is not edge.node2.comp: - raise DecompositionError( - 'Edge {0} node1.comp is not node2.comp'.format(edge) - ) - if edge.node1.comp not in component_map: - logger.warning( - 'Edge {0} node {1} is not in the component map'.format( - str(edge), str(edge.node1) - ) - ) - all_children = list(self.children) - assert len(all_children) == 2 - child0 = all_children[0] - child1 = all_children[1] - v1 = replacement_map_by_child[child0][edge.node1.comp] - v2 = replacement_map_by_child[child1][edge.node2.comp] - assert v1 is not v2 - block.linking_constraints.add(v1 == v2) - - return component_map def log(self, prefix=''): - logger.debug(prefix + '# Edges: {0}'.format(len(self.edges_between_children))) + logger.debug(prefix + '# Edges: {0}'.format(len(self.coupling_vars))) for _child in self.children: if isinstance(_child, _Tree): _child.log(prefix=prefix + ' ') @@ -418,8 +364,7 @@ def evaluate_partition(original_graph, tree): tree_nnz += child_nnz child_n_vars_to_tighten = len(collect_vars_to_tighten_from_graph(graph=child)) tree_obbt_nnz += child_nnz * child_n_vars_to_tighten - tree_nnz += 2 * len(tree.edges_between_children) - tree_obbt_nnz += tree_nnz * len(tree.edges_between_children) + tree_obbt_nnz += tree_nnz * len(tree.coupling_vars) partitioning_ratio = original_obbt_nnz / tree_obbt_nnz return partitioning_ratio @@ -434,9 +379,11 @@ def _refine_partition( con_count = defaultdict(int) for edge in removed_edges: n1, n2 = edge.node1, edge.node2 + assert n1.is_var() or n2.is_var() if n1.is_con(): + assert n2.is_var() con_count[n1.comp] += 1 - if n2.is_con(): + elif n2.is_con(): con_count[n2.comp] += 1 for c, count in con_count.items(): @@ -485,44 +432,38 @@ def _refine_partition( ) continue - # update the model - if not hasattr(model, 'dbt_partition_vars'): - model.dbt_partition_vars = pe.VarList() - model.dbt_partition_cons = pe.ConstraintList() - - graph_a_var = model.dbt_partition_vars.add() - graph_b_var = model.dbt_partition_vars.add() - - if c.lower is not None and c.upper is not None: - new_c1 = model.dbt_partition_cons.add(graph_a_var == sum(graph_a_args)) - new_c2 = model.dbt_partition_cons.add(graph_b_var == sum(graph_b_args)) - if c.equality: - c.set_value(graph_a_var + graph_b_var == c.lower) - else: - c.set_value((c.lower, graph_a_var + graph_b_var, c.upper)) - elif c.lower is None: - assert c.upper is not None - new_c1 = model.dbt_partition_cons.add(graph_a_var >= sum(graph_a_args)) - new_c2 = model.dbt_partition_cons.add(graph_b_var >= sum(graph_b_args)) - c.set_value(graph_a_var + graph_b_var <= c.upper) - else: - assert c.upper is None - new_c1 = model.dbt_partition_cons.add(graph_a_var <= sum(graph_a_args)) - new_c2 = model.dbt_partition_cons.add(graph_b_var <= sum(graph_b_args)) - c.set_value(graph_a_var + graph_b_var >= c.lower) - - # update the graph + graph_a_var = model.aux_vars.add() + graph_b_var = model.aux_vars.add() + model.vars.extend([graph_a_var, graph_b_var]) graph.remove_node(_ConNode(c)) graph.add_node(_VarNode(graph_a_var)) graph.add_node(_VarNode(graph_b_var)) - for new_con in [new_c1, new_c2, c]: - graph.add_node(_ConNode(new_con)) - for v in identify_variables(new_con.body, include_fixed=False): - graph.add_edge(_VarNode(v), _ConNode(new_con)) + + new_cons = list() + for e in [sum(graph_a_args) - graph_a_var, sum(graph_b_args) - graph_b_var]: + repn = generate_standard_repn(e) + if repn.is_linear(): + con_list = model.linear.cons + else: + con_list = model.nonlinear.cons + if c.lb is not None and c.ub is not None: + new_c = con_list.add(e == 0) + elif c.ub is not None: + new_c = con_list.add(e <= 0) + else: + new_c = con_list.add(e >= 0) + new_cons.append(new_c) + c.set_value((c.lb, graph_a_var + graph_b_var, c.ub)) + new_cons.append(c) + for new_c in new_cons: + graph.add_node(_ConNode(new_c)) + for v in identify_variables(new_c.body, include_fixed=False): + graph.add_edge(_VarNode(v), _ConNode(new_c)) # update removed_edges new_removed_edges = list() for e in removed_edges: + assert e.node1.is_var() if e.node2.comp is not c: new_removed_edges.append(e) @@ -534,8 +475,8 @@ def _refine_partition( graph_b_nodes.discard(_ConNode(c)) graph_a_nodes.add(_VarNode(graph_a_var)) graph_b_nodes.add(_VarNode(graph_b_var)) - graph_a_nodes.add(_ConNode(new_c1)) - graph_b_nodes.add(_ConNode(new_c2)) + graph_a_nodes.add(_ConNode(new_cons[0])) + graph_b_nodes.add(_ConNode(new_cons[1])) graph_b_nodes.add(_ConNode(c)) return removed_edges @@ -605,38 +546,19 @@ def split_metis(graph, model): graph_a_edges = list() graph_b_edges = list() for n1, n2 in graph.edges(): - if not n1.is_var(): - assert n2.is_var() - n1, n2 = n2, n1 - else: - assert not n2.is_var() - if n1 in graph_a_nodes and n2 in graph_a_nodes: + assert n1.is_var() + assert not n2.is_var() + if n2 in graph_a_nodes: graph_a_edges.append((n1, n2)) - elif n1 in graph_b_nodes and n2 in graph_b_nodes: - graph_b_edges.append((n1, n2)) else: - continue + graph_b_edges.append((n1, n2)) - linking_edges = list() - new_var_nodes_dict = dict() + linking_var_nodes = OrderedSet() for e in removed_edges: n1, n2 = e.node1, e.node2 assert n1.is_var() assert not n2.is_var() - if n1 in new_var_nodes_dict: - new_var_node = new_var_nodes_dict[n1] - else: - new_var_node = _VarNode(n1.comp) - new_var_nodes_dict[n1] = new_var_node - linking_edge = _Edge(n1, new_var_node) - linking_edges.append(linking_edge) - if n1 in graph_a_nodes: - assert n2 in graph_b_nodes - graph_b_edges.append((new_var_node, n2)) - else: - assert n1 in graph_b_nodes - assert n2 in graph_a_nodes - graph_a_edges.append((new_var_node, n2)) + linking_var_nodes.add(n1) graph_a = networkx.Graph() graph_b = networkx.Graph() @@ -649,9 +571,9 @@ def split_metis(graph, model): if (graph_a.number_of_nodes() >= 0.99 * graph.number_of_nodes()) or ( graph_b.number_of_nodes() >= 0.99 * graph.number_of_nodes() ): - raise DecompositionError('Failed to partition graph') + raise DecompositionError('Partition is extremely unbalanced') - tree = _Tree(children=[graph_a, graph_b], edges_between_children=linking_edges) + tree = _Tree(children=[graph_a, graph_b], coupling_vars=linking_var_nodes) partitioning_ratio = evaluate_partition(original_graph=graph, tree=tree) @@ -671,25 +593,23 @@ def convert_pyomo_model_to_bipartite_graph(m: _BlockData): graph = networkx.Graph() var_map = pe.ComponentMap() - for v in nonrelaxation_component_data_objects( - m, pe.Var, sort=True, descend_into=True - ): - if v.fixed: - continue - var_map[v] = _VarNode(v) - graph.add_node(var_map[v]) - - for b in relaxation_data_objects(m, descend_into=True, active=True, sort=True): + for b in relaxation_data_objects(m, descend_into=True, active=True): node2 = _RelNode(b) for v in list(b.get_rhs_vars()) + [b.get_aux_var()]: + if pe.is_fixed(v): + continue + if v not in var_map: + var_map[v] = _VarNode(v) node1 = var_map[v] graph.add_edge(node1, node2) for c in nonrelaxation_component_data_objects( - m, pe.Constraint, active=True, sort=True, descend_into=True + m, pe.Constraint, active=True, descend_into=True ): node2 = _ConNode(c) for v in identify_variables(c.body, include_fixed=False): + if v not in var_map: + var_map[v] = _VarNode(v) node1 = var_map[v] graph.add_edge(node1, node2) @@ -710,69 +630,30 @@ def build_pyomo_model_from_graph(graph, block): vars = list() cons = list() rels = list() - var_names = list() - con_names = list() - rel_names = list() for node in graph.nodes(): if node.is_var(): vars.append(node) - var_names.append(node.comp.getname(fully_qualified=True).replace('.', '_')) elif node.is_con(): cons.append(node) - con_names.append(node.comp.getname(fully_qualified=True).replace('.', '_')) else: assert node.is_rel() rels.append(node) - rel_names.append(node.comp.getname(fully_qualified=True).replace('.', '_')) assert len(vars) == len(set(vars)) assert len(cons) == len(set(cons)) assert len(rels) == len(set(rels)) - block.var_names = pe.Set(initialize=var_names) - block.con_names = pe.Set(initialize=con_names) - block.vars = pe.Var(block.var_names) - block.cons = pe.Constraint(block.con_names) + block.cons = pe.ConstraintList() block.rels = pe.Block() - component_map = pe.ComponentMap() - for v_name, v in zip(var_names, vars): - new_v = block.vars[v_name] - component_map[v.comp] = new_v - new_v.setlb(v.comp.lb) - new_v.setub(v.comp.ub) - new_v.domain = v.comp.domain - if v.comp.is_fixed(): - new_v.fix(v.comp.value) - new_v.set_value(v.comp.value, skip_validation=True) - - var_map = {id(k): v for k, v in component_map.items()} - - for c_name, c in zip(con_names, cons): - if c.comp.equality: - block.cons[c_name] = ( - replace_expressions( - c.comp.body, substitution_map=var_map, remove_named_expressions=True - ) - == c.comp.lower - ) - else: - block.cons[c_name] = pe.inequality( - lower=c.comp.lower, - body=replace_expressions( - c.comp.body, substitution_map=var_map, remove_named_expressions=True - ), - upper=c.comp.upper, - ) - component_map[c.comp] = block.cons[c_name] + for con_node in cons: + con = con_node.comp + block.cons.add((con.lb, con.body, con.ub)) - for r_name, r in zip(rel_names, rels): - new_rel = copy_relaxation_with_local_data(r.comp, var_map) - setattr(block.rels, r_name, new_rel) + for ndx, rel in enumerate(rels): + new_rel = copy_relaxation_with_local_data(rel.comp) + setattr(block.rels, f'rel{ndx}', new_rel) new_rel.rebuild() - component_map[r.comp] = new_rel - - return component_map def num_cons_in_graph(graph, include_rels=True): @@ -802,7 +683,7 @@ def compute_partition_ratio( ): graph = convert_pyomo_model_to_bipartite_graph(original_model) pr_numerator = graph.number_of_edges() * len( - collect_vars_to_tighten(original_model) + collect_vars_to_tighten_from_graph(graph) ) pr_denominator = 0 @@ -821,9 +702,10 @@ def _reformulate_objective(model): if current_obj is None: raise ValueError('No active objective found!') if not current_obj.expr.is_variable_type(): - obj_var_name = unique_component_name(model, 'obj_var') - obj_var = pe.Var(bounds=compute_bounds_on_expr(current_obj.expr)) - model.add_component(obj_var_name, obj_var) + obj_var = model.aux_vars.add() + lb, ub = compute_bounds_on_expr(current_obj.expr) + obj_var.setlb(lb) + obj_var.setub(ub) model.del_component(current_obj) new_objective = pe.Objective(expr=model.obj_var) new_obj_name = unique_component_name(model, 'objective') @@ -837,41 +719,6 @@ def _reformulate_objective(model): model.add_component(obj_con_name, obj_con) -def _eliminate_mutable_params(model): - sub_map = dict() - for p in nonrelaxation_component_data_objects(model, pe.Param, descend_into=True): - sub_map[id(p)] = p.value - - for c in nonrelaxation_component_data_objects( - model, pe.Constraint, active=True, descend_into=True - ): - if c.lower is None: - new_lower = None - else: - new_lower = replace_expressions( - c.lower, - sub_map, - descend_into_named_expressions=True, - remove_named_expressions=True, - ) - new_body = replace_expressions( - c.body, - sub_map, - descend_into_named_expressions=True, - remove_named_expressions=True, - ) - if c.upper is None: - new_upper = None - else: - new_upper = replace_expressions( - c.upper, - sub_map, - descend_into_named_expressions=True, - remove_named_expressions=True, - ) - c.set_value((new_lower, new_body, new_upper)) - - def _decompose_model( model: _BlockData, max_leaf_nnz: Optional[int] = None, @@ -905,8 +752,6 @@ def _decompose_model( # by reformulating the objective, we can make better use of the incumbent when # doing OBBT _reformulate_objective(model) - # we don't want the original param objects to be in the new model - _eliminate_mutable_params(model) graph = convert_pyomo_model_to_bipartite_graph(model) logger.debug('converted pyomo model to bipartite graph') @@ -926,8 +771,8 @@ def _decompose_model( else: logger.debug('Cannot decompose graph with less than 2 constraints.') new_model = TreeBlock(concrete=True) - new_model.setup(children_keys=list()) - component_map = build_pyomo_model_from_graph(graph=graph, block=new_model) + new_model.setup(children_keys=list(), coupling_vars=list()) + build_pyomo_model_from_graph(graph=graph, block=new_model) termination_reason = DecompositionStatus.problem_too_small logger.debug('done building pyomo model from graph') else: @@ -940,8 +785,8 @@ def _decompose_model( if partitioning_ratio < min_partition_ratio: logger.debug('obtained bad partitioning ratio; abandoning partition') new_model = TreeBlock(concrete=True) - new_model.setup(children_keys=list()) - component_map = build_pyomo_model_from_graph(graph=graph, block=new_model) + new_model.setup(children_keys=list(), coupling_vars=list()) + build_pyomo_model_from_graph(graph=graph, block=new_model) termination_reason = DecompositionStatus.bad_ratio logger.debug('done building pyomo model from graph') else: @@ -1012,23 +857,20 @@ def _decompose_model( root_tree.log() new_model = TreeBlock(concrete=True) - component_map = root_tree.build_pyomo_model(block=new_model) + root_tree.build_pyomo_model(block=new_model) logger.debug('done building pyomo model from tree') obj = get_objective(model) if obj is not None: - var_map = {id(k): v for k, v in component_map.items()} - new_model.objective = pe.Objective( - expr=replace_expressions( - obj.expr, substitution_map=var_map, remove_named_expressions=True - ), + new_model.obj = pe.Objective( + expr=obj.expr, sense=obj.sense, ) logger.debug('done adding objective to new model') else: logger.debug('No objective was found to add to the new model') - return new_model, component_map, termination_reason + return new_model, termination_reason def decompose_model( @@ -1055,41 +897,12 @@ def decompose_model( ------- new_model: TreeBlockData The decomposed model - component_map: pe.ComponentMap - A ComponentMap mapping variables and constraints in model to those in new_model termination_reason: DecompositionStatus An enum member from DecompositionStatus """ # we have to clone the model because we modify it in _refine_partition - all_comps = list( - ComponentSet( - nonrelaxation_component_data_objects(model, pe.Var, descend_into=True) - ) - ) - all_comps.extend( - ComponentSet( - nonrelaxation_component_data_objects( - model, pe.Constraint, active=True, descend_into=True - ) - ) - ) - all_comps.extend(relaxation_data_objects(model, descend_into=True, active=True)) - all_comps.extend( - ComponentSet( - nonrelaxation_component_data_objects( - model, pe.Objective, active=True, descend_into=True - ) - ) - ) - tmp_name = unique_component_name(model, 'all_comps') - setattr(model, tmp_name, all_comps) - new_model = model.clone() - old_to_new_comps_map = pe.ComponentMap( - zip(getattr(model, tmp_name), getattr(new_model, tmp_name)) - ) - delattr(model, tmp_name) - delattr(new_model, tmp_name) - model = new_model + model = clone_shallow_active_flat(model)[0] + model.aux_vars = pe.VarList() tmp = _decompose_model( model, @@ -1097,13 +910,9 @@ def decompose_model( min_partition_ratio=min_partition_ratio, limit_num_stages=limit_num_stages, ) - tree_model, component_map, termination_reason = tmp - - for orig_comp, clone_comp in list(old_to_new_comps_map.items()): - if clone_comp in component_map: - old_to_new_comps_map[orig_comp] = component_map[clone_comp] + tree_model, termination_reason = tmp - return tree_model, old_to_new_comps_map, termination_reason + return tree_model, termination_reason def collect_vars_to_tighten_from_graph(graph): @@ -1115,13 +924,11 @@ def collect_vars_to_tighten_from_graph(graph): if ( rel.is_rhs_convex() and rel.relaxation_side == RelaxationSide.UNDER - and not rel.use_linear_relaxation ): continue if ( rel.is_rhs_concave() and rel.relaxation_side == RelaxationSide.OVER - and not rel.use_linear_relaxation ): continue vars_to_tighten.update(rel.get_rhs_vars()) @@ -1155,6 +962,9 @@ def collect_vars_to_tighten_by_block(m, method): assert method in {'full_space', 'dbt', 'leaves'} vars_to_tighten_by_block = dict() + if method == 'full_space': + vars_to_tighten_by_block[m] = collect_vars_to_tighten(m) + return vars_to_tighten_by_block assert isinstance(m, TreeBlockData) @@ -1169,11 +979,7 @@ def collect_vars_to_tighten_by_block(m, method): elif method == 'full_space': vars_to_tighten_by_block[block] = ComponentSet() else: - vars_to_tighten_by_block[block] = ComponentSet() - for c in block.linking_constraints.values(): - if c.active: - vars_in_con = list(identify_variables(c.body)) - vars_to_tighten_by_block[block].add(vars_in_con[0]) + vars_to_tighten_by_block[block] = ComponentSet(block.coupling_vars) for block, vars_to_tighten in vars_to_tighten_by_block.items(): for v in vars_to_tighten: @@ -1625,44 +1431,6 @@ def perform_dbt( return dbt_info -def push_integers(block): - """ - Parameters - ---------- - block: pyomo.core.base.block._BlockData - The block for which integer variables should be relaxed. - - Returns - ------- - relaxed_binary_vars: ComponentSet of pyomo.core.base.var._GeneralVarData - relaxed_integer_vars: ComponentSet or pyomo.core.base.var._GeneralVarData - """ - relaxed_binary_vars = ComponentSet() - relaxed_integer_vars = ComponentSet() - for v in block.component_data_objects(pe.Var, descend_into=True, sort=True): - if v.fixed: - continue - if v.is_binary(): - relaxed_binary_vars.add(v) - orig_lb = v.lb - orig_ub = v.ub - v.domain = pe.Reals - v.setlb(orig_lb) - v.setub(orig_ub) - elif v.is_integer(): - relaxed_integer_vars.add(v) - v.domain = pe.Reals - - return relaxed_binary_vars, relaxed_integer_vars - - -def pop_integers(relaxed_binary_vars, relaxed_integer_vars): - for v in relaxed_binary_vars: - v.domain = pe.Binary - for v in relaxed_integer_vars: - v.domain = pe.Integers - - def perform_dbt_with_integers_relaxed( relaxation, solver, diff --git a/pyomo/contrib/coramin/domain_reduction/filters.py b/pyomo/contrib/coramin/domain_reduction/filters.py index 53e6fc31b2f..4898ef067e5 100644 --- a/pyomo/contrib/coramin/domain_reduction/filters.py +++ b/pyomo/contrib/coramin/domain_reduction/filters.py @@ -125,7 +125,7 @@ def aggressive_filter( return vars_to_minimize, vars_to_maximize tmp = _bt_prep(model=relaxation, solver=solver, objective_bound=objective_bound) - initial_var_values, deactivated_objectives, orig_update_config, orig_config = tmp + deactivated_objectives, orig_update_config, orig_config = tmp vars_unbounded_from_below = ComponentSet() vars_unbounded_from_above = ComponentSet() @@ -190,7 +190,6 @@ def aggressive_filter( model=relaxation, solver=solver, vardatalist=None, - initial_var_values=initial_var_values, deactivated_objectives=deactivated_objectives, orig_update_config=orig_update_config, orig_config=orig_config, diff --git a/pyomo/contrib/coramin/domain_reduction/obbt.py b/pyomo/contrib/coramin/domain_reduction/obbt.py index 0ee07572b36..5272d2ccc1e 100644 --- a/pyomo/contrib/coramin/domain_reduction/obbt.py +++ b/pyomo/contrib/coramin/domain_reduction/obbt.py @@ -39,7 +39,6 @@ def _bt_cleanup( model, solver: Union[appsi.base.Solver, appsi.base.PersistentSolver], vardatalist: Optional[List[_GeneralVarData]], - initial_var_values, deactivated_objectives, orig_update_config, orig_config, @@ -68,11 +67,6 @@ def _bt_cleanup( Only needed if you want to update the bounds of the variables. Should be in the same order as self.vars_to_tighten. """ - for v in model.component_data_objects( - ctype=pyo.Var, active=None, sort=True, descend_into=True - ): - v.set_value(initial_var_values[v], skip_validation=True) - if hasattr(model, '__objective_ineq'): if solver.is_persistent(): solver.remove_constraints([model.__objective_ineq]) @@ -282,7 +276,6 @@ def _tighten_bnds( def _bt_prep(model, solver, objective_bound=None): """ Prepare the model for bounds tightening. - Gather the variable values to load back in after bounds tightening. Deactivate any active objectives. If objective_ub is not None, then add a constraint forcing the objective to be less than objective_ub @@ -323,12 +316,6 @@ def _bt_prep(model, solver, objective_bound=None): if solver.is_persistent(): solver.set_instance(model) - initial_var_values = ComponentMap() - for v in model.component_data_objects( - ctype=pyo.Var, active=None, sort=True, descend_into=True - ): - initial_var_values[v] = v.value - deactivated_objectives = list() for obj in model.component_data_objects( pyo.Objective, active=True, sort=True, descend_into=True @@ -359,7 +346,7 @@ def _bt_prep(model, solver, objective_bound=None): if solver.is_persistent(): solver.add_constraints([model.__objective_ineq]) - return initial_var_values, deactivated_objectives, orig_update_config, orig_config + return deactivated_objectives, orig_update_config, orig_config def _build_vardatalist(model, varlist=None, warning_threshold=0): @@ -413,7 +400,7 @@ def _build_vardatalist(model, varlist=None, warning_threshold=0): def perform_obbt( model, solver, - varlist=None, + varlist, objective_bound=None, update_bounds=True, with_progress_bar=False, @@ -470,7 +457,6 @@ def perform_obbt( t0 = time.time() ( - initial_var_values, deactivated_objectives, orig_update_config, orig_config, @@ -596,7 +582,6 @@ def perform_obbt( model=model, solver=solver, vardatalist=vardata_list, - initial_var_values=initial_var_values, deactivated_objectives=deactivated_objectives, orig_update_config=orig_update_config, orig_config=orig_config, diff --git a/pyomo/contrib/coramin/relaxations/copy_relaxation.py b/pyomo/contrib/coramin/relaxations/copy_relaxation.py index a4e860c99c9..01a09439620 100644 --- a/pyomo/contrib/coramin/relaxations/copy_relaxation.py +++ b/pyomo/contrib/coramin/relaxations/copy_relaxation.py @@ -19,7 +19,7 @@ from pyomo.contrib.coramin.utils.coramin_enums import FunctionShape -def copy_relaxation_with_local_data(rel, old_var_to_new_var_map): +def copy_relaxation_with_local_data(rel, old_var_to_new_var_map=None): """ This function copies a relaxation object with new variables. Note that only what can be set through the set_input and build @@ -38,6 +38,13 @@ def copy_relaxation_with_local_data(rel, old_var_to_new_var_map): rel: coramin.relaxations.relaxations_base.BaseRelaxationData The copy of rel with new variables """ + if old_var_to_new_var_map is None: + old_var_to_new_var_map = dict() + for v in rel.get_rhs_vars(): + old_var_to_new_var_map[id(v)] = v + aux_var = rel.get_aux_var() + old_var_to_new_var_map[id(aux_var)] = aux_var + if isinstance(rel, PWXSquaredRelaxationData): new_x = old_var_to_new_var_map[id(rel.get_rhs_vars()[0])] new_aux_var = old_var_to_new_var_map[id(rel.get_aux_var())] From c7bf0efe9a12f7882bc12f1802c6219956301d83 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 30 Jan 2024 22:38:07 -0700 Subject: [PATCH 086/128] add utility to compare feasible regions of two models --- pyomo/contrib/coramin/utils/compare_models.py | 68 +++++++++++++++++++ pyomo/contrib/coramin/utils/tests/__init__.py | 0 .../utils/tests/test_compare_models.py | 25 +++++++ 3 files changed, 93 insertions(+) create mode 100644 pyomo/contrib/coramin/utils/compare_models.py create mode 100644 pyomo/contrib/coramin/utils/tests/__init__.py create mode 100644 pyomo/contrib/coramin/utils/tests/test_compare_models.py diff --git a/pyomo/contrib/coramin/utils/compare_models.py b/pyomo/contrib/coramin/utils/compare_models.py new file mode 100644 index 00000000000..a26b891d57b --- /dev/null +++ b/pyomo/contrib/coramin/utils/compare_models.py @@ -0,0 +1,68 @@ +import pyomo.environ as pe +from pyomo.contrib.coramin.clone import clone_shallow_active_flat +from pyomo.core.base.block import _BlockData +from typing import Optional +from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr +from pyomo.contrib import appsi + + +def is_relaxation(a: _BlockData, b: _BlockData, opt: appsi.base.Solver, feasibility_tol: float = 1e-6, bigM: Optional[float] = None): + """ + Returns True if every feasible point in b is feasible for a + (a is a relaxation of b) + a and b should share variables + """ + m = clone_shallow_active_flat(b)[0] + if hasattr(m.linear, 'obj'): + del m.linear.obj + if hasattr(m.nonlinear, 'obj'): + del m.nonlinear.obj + + m.max_viol = pe.Var(bounds=(None, 1)) + m.con_viol = pe.VarList() + m.is_max = pe.VarList(domain=pe.Binary) + m.max_viol_cons = pe.ConstraintList() + u_y_pairs = list() + default_M = bigM + if default_M is None: + bigM = 0 + else: + bigM = default_M + for con in a.component_data_objects(pe.Constraint, descend_into=True, active=True): + elist = list() + if con.ub is not None: + elist.append(con.body - con.ub) + if con.lb is not None: + elist.append(-con.body + con.lb) + for e in elist: + u = m.con_viol.add() + y = m.is_max.add() + m.max_viol_cons.add(u <= e) + u_y_pairs.append((u, y)) + if default_M is None: + u_lb = compute_bounds_on_expr(e)[0] + if u_lb is None: + raise RuntimeError('could not compute big M value') + if u_lb > feasibility_tol: + return False + if u_lb < 0: + bigM = max(bigM, abs(u_lb)) + m.max_viol_cons.add(sum(m.is_max.values()) == 1) + for u, y in u_y_pairs: + m.max_viol_cons.add(m.max_viol <= u + (1 - y) * bigM) + m.obj = pe.Objective(expr=m.max_viol, sense=pe.maximize) + + res = opt.solve(m) + assert res.termination_condition == appsi.base.TerminationCondition.optimal + + return res.best_feasible_objective <= feasibility_tol + + +def is_equivalent(a: _BlockData, b: _BlockData, opt: appsi.base.Solver, feasibility_tol: float = 1e-6, bigM: Optional[float] = None): + """ + Returns True if the feasible regions of a and b are the same + a and b should share variables + """ + cond1 = is_relaxation(a, b, opt=opt, feasibility_tol=feasibility_tol, bigM=bigM) + cond2 = is_relaxation(b, a, opt=opt, feasibility_tol=feasibility_tol, bigM=bigM) + return cond1 and cond2 diff --git a/pyomo/contrib/coramin/utils/tests/__init__.py b/pyomo/contrib/coramin/utils/tests/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/pyomo/contrib/coramin/utils/tests/test_compare_models.py b/pyomo/contrib/coramin/utils/tests/test_compare_models.py new file mode 100644 index 00000000000..5c8dcecaad9 --- /dev/null +++ b/pyomo/contrib/coramin/utils/tests/test_compare_models.py @@ -0,0 +1,25 @@ +import pyomo.environ as pe +from pyomo.contrib import appsi +from pyomo.common import unittest +from pyomo.contrib.coramin.utils.compare_models import is_relaxation, is_equivalent + + +class TestCompareModels(unittest.TestCase): + def test_compare_models_1(self): + m1 = pe.ConcreteModel() + m1.x = x = pe.Var(bounds=(-5, 4)) + m1.y = y = pe.Var(bounds=(0, 7)) + + m1.c1 = pe.Constraint(expr=x + y == 1) + + m2 = pe.ConcreteModel() + m2.c1 = pe.Constraint(expr=x + y <= 1) + m2.c2 = pe.Constraint(expr=x + y >= 1) + + opt = appsi.solvers.Highs() + + self.assertTrue(is_equivalent(m1, m2, opt)) + m2.c2.deactivate() + self.assertFalse(is_equivalent(m1, m2, opt)) + self.assertTrue(is_relaxation(m2, m1, opt)) + self.assertFalse(is_relaxation(m1, m2, opt)) From 1dfb506613b7bd19df9d07d5cd8b525a09ab002e Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 30 Jan 2024 22:39:48 -0700 Subject: [PATCH 087/128] coramin cleanup --- .../algorithms/tests/test_ecp_bounder.py | 4 +- pyomo/contrib/coramin/domain_reduction/dbt.py | 48 +++++++++++-------- 2 files changed, 31 insertions(+), 21 deletions(-) diff --git a/pyomo/contrib/coramin/algorithms/tests/test_ecp_bounder.py b/pyomo/contrib/coramin/algorithms/tests/test_ecp_bounder.py index 061b9eb6b62..a4b17c8d877 100644 --- a/pyomo/contrib/coramin/algorithms/tests/test_ecp_bounder.py +++ b/pyomo/contrib/coramin/algorithms/tests/test_ecp_bounder.py @@ -17,9 +17,9 @@ def test_ecp_bounder(self): m.obj = pe.Objective(expr=0.5 * (m.x**2 + m.y**2)) m.c1 = pe.Constraint(expr=m.y >= (m.x - 1) ** 2) m.c2 = pe.Constraint(expr=m.y >= pe.exp(m.x)) - coramin.relaxations.relax(m) + r = coramin.relaxations.relax(m) opt = ECPBounder(subproblem_solver=appsi.solvers.Gurobi()) - res = opt.solve(m) + res = opt.solve(r) self.assertEqual( res.termination_condition, appsi.base.TerminationCondition.optimal ) diff --git a/pyomo/contrib/coramin/domain_reduction/dbt.py b/pyomo/contrib/coramin/domain_reduction/dbt.py index b41ae353c7d..c236bf0ed41 100644 --- a/pyomo/contrib/coramin/domain_reduction/dbt.py +++ b/pyomo/contrib/coramin/domain_reduction/dbt.py @@ -105,6 +105,10 @@ def children(self): @property def coupling_vars(self): self._assert_setup() + if self.is_leaf(): + raise TreeBlockError( + 'Leaf TreeBlocks do not have coupling variables. Please check the is_leaf method' + ) return list(self._coupling_vars) def _num_stages(self): @@ -248,15 +252,21 @@ def build_pyomo_model(self, block): else: raise ValueError('Unexpected child type: {0}'.format(str(type(child)))) - def log(self, prefix=''): - logger.debug(prefix + '# Edges: {0}'.format(len(self.coupling_vars))) + def to_string(self, prefix=''): + s = '' + s += f'{prefix}# Edges: {len(self.coupling_vars)}\n' for _child in self.children: if isinstance(_child, _Tree): - _child.log(prefix=prefix + ' ') + s += _child.to_string(prefix=prefix + ' ') else: - logger.debug( - prefix + ' Leaf: # NNZ: {0}'.format(_child.number_of_edges()) - ) + s += f'{prefix} Leaf: # NNZ: {_child.number_of_edges()}\n' + return s + + def log(self, prefix=''): + logger.debug(self.to_string(prefix=prefix)) + + def __str__(self): + return self.to_string() def _is_dominated(ndx, num_cuts, balance, num_cuts_array, balance_array): @@ -379,11 +389,10 @@ def _refine_partition( con_count = defaultdict(int) for edge in removed_edges: n1, n2 = edge.node1, edge.node2 - assert n1.is_var() or n2.is_var() - if n1.is_con(): - assert n2.is_var() - con_count[n1.comp] += 1 - elif n2.is_con(): + assert n1.is_var() != n2.is_var() # xor + if n2.is_var(): + n1, n2 = n2, n1 + if n2.is_con(): # n2 might be a rel con_count[n2.comp] += 1 for c, count in con_count.items(): @@ -523,11 +532,9 @@ def split_metis(graph, model): removed_edges = list() for n1, n2 in graph.edges(): - if not n1.is_var(): - assert n2.is_var() + assert n1.is_var() != n2.is_var() # xor + if n2.is_var(): n1, n2 = n2, n1 - else: - assert not n2.is_var() if n1 in graph_a_nodes and n2 in graph_a_nodes: continue elif n1 in graph_b_nodes and n2 in graph_b_nodes: @@ -546,18 +553,21 @@ def split_metis(graph, model): graph_a_edges = list() graph_b_edges = list() for n1, n2 in graph.edges(): - assert n1.is_var() - assert not n2.is_var() + assert n1.is_var() != n2.is_var() # xor + if n2.is_var(): + n1, n2, = n2, n1 if n2 in graph_a_nodes: graph_a_edges.append((n1, n2)) else: + assert n2 in graph_b_nodes graph_b_edges.append((n1, n2)) linking_var_nodes = OrderedSet() for e in removed_edges: n1, n2 = e.node1, e.node2 - assert n1.is_var() - assert not n2.is_var() + assert n1.is_var() != n2.is_var() # xor + if n2.is_var(): + n1, n2 = n2, n1 linking_var_nodes.add(n1) graph_a = networkx.Graph() From ac7a40d13ed54558af61154849e4fd610eb46afd Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 31 Jan 2024 11:03:14 -0700 Subject: [PATCH 088/128] coramin: working on tests --- pyomo/contrib/coramin/clone.py | 6 +- pyomo/contrib/coramin/domain_reduction/dbt.py | 24 +-- .../domain_reduction/tests/test_dbt.py | 153 ++++++++++++++---- pyomo/contrib/coramin/utils/compare_models.py | 90 ++++++++++- pyomo/contrib/coramin/utils/pyomo_utils.py | 11 +- .../utils/tests/test_compare_models.py | 77 ++++++++- 6 files changed, 313 insertions(+), 48 deletions(-) diff --git a/pyomo/contrib/coramin/clone.py b/pyomo/contrib/coramin/clone.py index bf21e05a71f..d0bb9516dfc 100644 --- a/pyomo/contrib/coramin/clone.py +++ b/pyomo/contrib/coramin/clone.py @@ -35,7 +35,7 @@ def get_clone_and_var_map(m1: _BlockData): return m2, var_map -def clone_shallow_active_flat(m1: _BlockData, num_clones: int = 1) -> List[_BlockData]: +def clone_shallow_active_flat(m1: _BlockData, num_clones: int = 1, clone_expressions: bool = False) -> List[_BlockData]: clone_list = [pe.Block(concrete=True) for i in range(num_clones)] for m2 in clone_list: m2.linear = pe.Block() @@ -52,6 +52,8 @@ def clone_shallow_active_flat(m1: _BlockData, num_clones: int = 1) -> List[_Bloc all_vars.update(repn.linear_vars) all_vars.update(repn.nonlinear_vars) body = repn.to_expression() + if clone_expressions: + body = body.clone() if repn.nonlinear_expr is None: for m2 in clone_list: m2.linear.cons.add((c.lb, body, c.ub)) @@ -66,6 +68,8 @@ def clone_shallow_active_flat(m1: _BlockData, num_clones: int = 1) -> List[_Bloc all_vars.update(repn.linear_vars) all_vars.update(repn.nonlinear_vars) obj_expr = repn.to_expression() + if clone_expressions: + obj_expr = obj_expr.clone() if repn.nonlinear_expr is None: for m2 in clone_list: m2.linear.obj = pe.Objective(expr=obj_expr, sense=obj.sense) diff --git a/pyomo/contrib/coramin/domain_reduction/dbt.py b/pyomo/contrib/coramin/domain_reduction/dbt.py index c236bf0ed41..3ed82f1198d 100644 --- a/pyomo/contrib/coramin/domain_reduction/dbt.py +++ b/pyomo/contrib/coramin/domain_reduction/dbt.py @@ -375,7 +375,10 @@ def evaluate_partition(original_graph, tree): child_n_vars_to_tighten = len(collect_vars_to_tighten_from_graph(graph=child)) tree_obbt_nnz += child_nnz * child_n_vars_to_tighten tree_obbt_nnz += tree_nnz * len(tree.coupling_vars) - partitioning_ratio = original_obbt_nnz / tree_obbt_nnz + if tree_obbt_nnz > 0: + partitioning_ratio = original_obbt_nnz / tree_obbt_nnz + else: + partitioning_ratio = None return partitioning_ratio @@ -401,7 +404,7 @@ def _refine_partition( new_body = flatten_expr(c.body) - if type(new_body) is not numeric_expr.SumExpression: + if type(new_body) not in {numeric_expr.SumExpression, numeric_expr.LinearExpression}: logger.info( f'Constraint {str(c)} is contributing to {count} removed ' f'edges, but we cannot split the constraint because the ' @@ -710,7 +713,7 @@ def compute_partition_ratio( def _reformulate_objective(model): current_obj = get_objective(model) if current_obj is None: - raise ValueError('No active objective found!') + return None if not current_obj.expr.is_variable_type(): obj_var = model.aux_vars.add() lb, ub = compute_bounds_on_expr(current_obj.expr) @@ -732,7 +735,7 @@ def _reformulate_objective(model): def _decompose_model( model: _BlockData, max_leaf_nnz: Optional[int] = None, - min_partition_ratio: float = 1.25, + min_partition_ratio: float = 0, limit_num_stages: bool = True, ): """ @@ -762,6 +765,9 @@ def _decompose_model( # by reformulating the objective, we can make better use of the incumbent when # doing OBBT _reformulate_objective(model) + new_model = TreeBlock(concrete=True) + new_model.aux_vars = pe.VarList() + object.__setattr__(model, 'aux_vars', new_model.aux_vars) graph = convert_pyomo_model_to_bipartite_graph(model) logger.debug('converted pyomo model to bipartite graph') @@ -780,7 +786,6 @@ def _decompose_model( logger.debug('too few NNZ in original graph; not decomposing') else: logger.debug('Cannot decompose graph with less than 2 constraints.') - new_model = TreeBlock(concrete=True) new_model.setup(children_keys=list(), coupling_vars=list()) build_pyomo_model_from_graph(graph=graph, block=new_model) termination_reason = DecompositionStatus.problem_too_small @@ -792,9 +797,8 @@ def _decompose_model( ratio=partitioning_ratio ) ) - if partitioning_ratio < min_partition_ratio: + if min_partition_ratio > 0 and partitioning_ratio < min_partition_ratio: logger.debug('obtained bad partitioning ratio; abandoning partition') - new_model = TreeBlock(concrete=True) new_model.setup(children_keys=list(), coupling_vars=list()) build_pyomo_model_from_graph(graph=graph, block=new_model) termination_reason = DecompositionStatus.bad_ratio @@ -835,7 +839,7 @@ def _decompose_model( logger.debug( 'partitioning ratio: {ratio}'.format(ratio=partitioning_ratio) ) - if partitioning_ratio > min_partition_ratio: + if min_partition_ratio <= 0 or partitioning_ratio > min_partition_ratio: logger.debug('partitioned {0}'.format(str(_graph))) _parent.children.discard(_graph) _parent.children.add(sub_tree) @@ -866,7 +870,6 @@ def _decompose_model( logger.debug('Tree Info:') root_tree.log() - new_model = TreeBlock(concrete=True) root_tree.build_pyomo_model(block=new_model) logger.debug('done building pyomo model from tree') @@ -886,7 +889,7 @@ def _decompose_model( def decompose_model( model: _BlockData, max_leaf_nnz: Optional[int] = None, - min_partition_ratio: float = 1.25, + min_partition_ratio: float = 0, limit_num_stages: bool = True, ): """ @@ -912,7 +915,6 @@ def decompose_model( """ # we have to clone the model because we modify it in _refine_partition model = clone_shallow_active_flat(model)[0] - model.aux_vars = pe.VarList() tmp = _decompose_model( model, diff --git a/pyomo/contrib/coramin/domain_reduction/tests/test_dbt.py b/pyomo/contrib/coramin/domain_reduction/tests/test_dbt.py index 70cfbfd6159..5c4bfeeba86 100644 --- a/pyomo/contrib/coramin/domain_reduction/tests/test_dbt.py +++ b/pyomo/contrib/coramin/domain_reduction/tests/test_dbt.py @@ -12,6 +12,7 @@ perform_dbt, OBBTMethod, FilterMethod, + DecompositionStatus, ) from pyomo.common import unittest import pyomo.environ as pe @@ -29,6 +30,111 @@ import filecmp from pyomo.contrib import appsi import pytest +from pyomo.contrib.coramin.utils.compare_models import is_relaxation, is_equivalent +from pyomo.contrib.coramin.utils.pyomo_utils import active_cons, active_vars + + +class TestDecomposition(unittest.TestCase): + def test_decomp1(self): + m1 = pe.ConcreteModel() + m1.x = x = pe.Var([1, 2, 3, 4, 5, 6], bounds=(-10, 10)) + m1.c = c = pe.ConstraintList() + + c.add(x[1] == x[2] + x[3]) + c.add(x[4] == x[5] + x[6]) + c.add(x[2] <= 2*x[3] + 1) + c.add(x[5] >= 2*x[6] + 1) + + m2, reason = decompose_model(m1) + self.assertEqual(reason, DecompositionStatus.normal) + self.assertTrue(is_equivalent(m1, m2, appsi.solvers.Highs())) + self.assertEqual(len(m2.children), 2) + self.assertEqual(len(list(active_cons(m2.children[0]))), 2) + self.assertEqual(len(list(active_cons(m2.children[1]))), 2) + self.assertEqual(len(list(active_vars(m2.children[0]))), 3) + self.assertEqual(len(list(active_vars(m2.children[1]))), 3) + self.assertEqual(m2.get_block_stage(m2), 0) + self.assertEqual(m2.get_block_stage(m2.children[0]), 1) + self.assertEqual(m2.get_block_stage(m2.children[1]), 1) + self.assertEqual(list(m2.stage_blocks(0)), [m2]) + self.assertEqual(list(m2.stage_blocks(1)), [m2.children[0], m2.children[1]]) + + def test_decomp2(self): + m1 = pe.ConcreteModel() + m1.x = x = pe.Var([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], bounds=(-10, 10)) + m1.c = c = pe.ConstraintList() + + c.add(x[1] == x[2] + x[3]) + c.add(x[4] == x[5] + x[6]) + c.add(x[2] <= 2*x[3] + 1) + c.add(x[5] >= 2*x[6] + 1) + c.add(x[1] == x[4]) + + c.add(x[7] == x[8] + x[9]) + c.add(x[10] == x[11] + x[12]) + c.add(x[8] <= 2*x[9] + 1) + c.add(x[11] >= 2*x[12] + 1) + c.add(x[7] == x[10]) + + m2, reason = decompose_model(m1, limit_num_stages=False) + self.assertEqual(reason, DecompositionStatus.normal) + self.assertTrue(is_equivalent(m1, m2, appsi.solvers.Highs())) + self.assertEqual(len(m2.children), 2) + self.assertEqual(len(list(active_cons(m2.children[0]))), 5) + self.assertEqual(len(list(active_cons(m2.children[1]))), 5) + self.assertEqual(len(list(active_vars(m2.children[0]))), 6) + self.assertEqual(len(list(active_vars(m2.children[1]))), 6) + self.assertEqual(m2.get_block_stage(m2), 0) + self.assertEqual(m2.get_block_stage(m2.children[0]), 1) + self.assertEqual(m2.get_block_stage(m2.children[1]), 1) + self.assertEqual(list(m2.stage_blocks(0)), [m2]) + self.assertEqual(list(m2.stage_blocks(1)), [m2.children[0], m2.children[1]]) + self.assertEqual(list(m2.stage_blocks(2)), [m2.children[0].children[0], m2.children[0].children[1], m2.children[1].children[0], m2.children[1].children[1]]) + + for b in [m2.children[0], m2.children[1]]: + self.assertEqual(len(b.children), 2) + self.assertIn(len(list(active_cons(b.children[0]))), {2, 3}) + self.assertIn(len(list(active_cons(b.children[1]))), {2, 3}) + self.assertIn(len(list(active_vars(b.children[0]))), {3, 4}) + self.assertIn(len(list(active_vars(b.children[1]))), {3, 4}) + self.assertEqual(m2.get_block_stage(b), 1) + self.assertEqual(m2.get_block_stage(b.children[0]), 2) + self.assertEqual(m2.get_block_stage(b.children[1]), 2) + self.assertEqual(len(b.coupling_vars), 1) + + def test_refine_partition(self): + m1 = pe.ConcreteModel() + m1.x = x = pe.Var([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], bounds=(-10, 10)) + m1.c = c = pe.ConstraintList() + + c.add(x[1] == x[2] + x[3]) + c.add(x[4] == x[5] + x[6]) + c.add(x[2] <= 2*x[3] + 1) + c.add(x[5] >= 2*x[6] + 1) + c.add(x[1] == x[4]) + + c.add(x[7] == x[8] + x[9]) + c.add(x[10] == x[11] + x[12]) + c.add(x[8] <= 2*x[9] + 1) + c.add(x[11] >= 2*x[12] + 1) + c.add(x[7] == x[10]) + + c.add(sum(x.values()) == 1) + m2, reason = decompose_model(m1) + m2.pprint() + self.assertEqual(reason, DecompositionStatus.normal) + opt = appsi.solvers.Highs() + opt.config.stream_solver = True + self.assertTrue(is_relaxation(m1, m2, appsi.solvers.Highs(), bigM=1000)) + self.assertTrue(is_relaxation(m2, m1, opt, bigM=1000)) + self.assertTrue(is_equivalent(m1, m2, appsi.solvers.Highs(), bigM=1000)) + self.assertEqual(len(m2.children), 2) + self.assertEqual(len(m2.coupling_vars), 1) + self.assertIn(len(list(active_cons(m2.children[0]))), {6, 7}) + self.assertIn(len(list(active_cons(m2.children[1]))), {6, 7}) + self.assertEqual(len(m2.coupling_vars), 1) + self.assertIn(len(list(active_vars(m2.children[0]))), {7, 8}) + self.assertIn(len(list(active_vars(m2.children[1]))), {7, 8}) class TestTreeBlock(unittest.TestCase): @@ -37,25 +143,23 @@ def test_tree_block(self): with self.assertRaises(TreeBlockError): b.is_leaf() with self.assertRaises(TreeBlockError): - b.x = pe.Var() + b.children with self.assertRaises(TreeBlockError): - children = b.children + b.coupling_vars with self.assertRaises(TreeBlockError): - linking_constraints = b.linking_constraints + b.num_stages() with self.assertRaises(TreeBlockError): - num_stages = b.num_stages() + list(b.stage_blocks(0)) with self.assertRaises(TreeBlockError): - stage_blocks = list(b.stage_blocks(0)) - with self.assertRaises(TreeBlockError): - stage = b.get_block_stage(b) - b.setup(children_keys=list()) + b.get_block_stage(b) + b.setup(children_keys=list(), coupling_vars=list()) self.assertTrue(b.is_leaf()) b.x = pe.Var() # make sure we can add components just like a regular block b.x.setlb(-1) with self.assertRaises(TreeBlockError): - linking_constraints = b.linking_constraints + b.coupling_vars with self.assertRaises(TreeBlockError): - children = b.children + b.children self.assertEqual(b.num_stages(), 1) stage0_blocks = list(b.stage_blocks(0)) self.assertEqual(len(stage0_blocks), 1) @@ -65,22 +169,18 @@ def test_tree_block(self): self.assertEqual(b.get_block_stage(b), 0) b = TreeBlock(concrete=True) - b.setup(children_keys=[1, 2]) - b.children[1].setup(children_keys=list()) - b.children[2].setup(children_keys=['a', 'b']) - b.children[2].children['a'].setup(children_keys=list()) - b.children[2].children['b'].setup(children_keys=list()) + b.setup(children_keys=[1, 2], coupling_vars=list()) + b.children[1].setup(children_keys=list(), coupling_vars=list()) + b.children[2].setup(children_keys=['a', 'b'], coupling_vars=list()) + b.children[2].children['a'].setup(children_keys=list(), coupling_vars=list()) + b.children[2].children['b'].setup(children_keys=list(), coupling_vars=list()) self.assertFalse(b.is_leaf()) self.assertTrue(b.children[1].is_leaf()) self.assertFalse(b.children[2].is_leaf()) self.assertTrue(b.children[2].children['a'].is_leaf()) self.assertTrue(b.children[2].children['b'].is_leaf()) - with self.assertRaises(TreeBlockError): - b.x = pe.Var() b.children[1].x = pe.Var() - with self.assertRaises(TreeBlockError): - b.children[2].x = pe.Var() b.children[2].children['a'].x = pe.Var() b.children[2].children['b'].x = pe.Var() self.assertEqual( @@ -131,7 +231,7 @@ def test_tree_block(self): with self.assertRaises(TreeBlockError): b.children[2].get_block_stage(b.children[2].children['a']) - self.assertEqual(len(list(b.linking_constraints.values())), 0) + self.assertEqual(len(b.coupling_vars), 0) class TestGraphConversion(unittest.TestCase): @@ -245,7 +345,7 @@ def test_split_metis(self): g.add_edge(v6, c2) tree, partitioning_ratio = split_metis(graph=g, model=m) - self.assertAlmostEqual(partitioning_ratio, 3 * 12 / (14 * 1 + 6 * 2 + 6 * 2)) + self.assertAlmostEqual(partitioning_ratio, 3 * 12 / (12 * 1 + 6 * 2 + 6 * 2)) children = list(tree.children) self.assertEqual(len(children), 2) @@ -287,13 +387,10 @@ def test_split_metis(self): self.assertEqual(len(graph_a_edges), 6) self.assertEqual(len(graph_b_edges), 6) - edges_between_children = list(tree.edges_between_children) - self.assertEqual(len(edges_between_children), 1) - edge = edges_between_children[0] - self.assertTrue( - (v4 is edge.node1 and v4_hat is edge.node2) - or (v4 is edge.node2 and v4_hat is edge.node1) - ) + coupling_vars = list(tree.coupling_vars) + self.assertEqual(len(coupling_vars), 1) + cv = coupling_vars[0] + self.assertEqual(v4, cv) new_model = TreeBlock(concrete=True) component_map = tree.build_pyomo_model(block=new_model) diff --git a/pyomo/contrib/coramin/utils/compare_models.py b/pyomo/contrib/coramin/utils/compare_models.py index a26b891d57b..b47b0951b0b 100644 --- a/pyomo/contrib/coramin/utils/compare_models.py +++ b/pyomo/contrib/coramin/utils/compare_models.py @@ -4,6 +4,78 @@ from typing import Optional from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr from pyomo.contrib import appsi +from .pyomo_utils import active_vars, active_cons, simplify_expr +from pyomo.common.collections import ComponentSet, ComponentMap, OrderedSet +from pyomo.core.expr.visitor import identify_variables, replace_expressions +from pyomo.repn.standard_repn import StandardRepn, generate_standard_repn +from pyomo.common.modeling import unique_component_name + + +def _attempt_presolve(m, vars_to_presolve): + vars_to_presolve = ComponentSet(vars_to_presolve) + var_to_con_map = ComponentMap() + for v in vars_to_presolve: + var_to_con_map[v] = OrderedSet() + for c in active_cons(m): + for v in identify_variables(c.body, include_fixed=False): + if v in vars_to_presolve: + var_to_con_map[v].add(c) + cname = unique_component_name(m, 'bound_constraints') + bound_cons = pe.ConstraintList() + setattr(m, cname, bound_cons) + for v in list(var_to_con_map.keys()): + con_list = var_to_con_map[v] + v_expr = None + v_con = None + v_repn = None + v_vars = None + density = None + for c in con_list: + if not c.equality: + continue + if not c.active: + continue + repn: StandardRepn = generate_standard_repn(c.body - c.lb, compute_values=True, quadratic=False) + lin_vars = ComponentSet(repn.linear_vars) + nonlin_vars = ComponentSet(repn.nonlinear_vars) + if v in lin_vars and v not in nonlin_vars: + n_vars = len(ComponentSet(list(repn.linear_vars) + list(repn.nonlinear_vars))) + if density is None or n_vars < density: + v_expr = -repn.constant + for coef, other in zip(repn.linear_coefs, repn.linear_vars): + if v is other: + v_coef = coef + else: + v_expr -= coef * other + if repn.nonlinear_expr is not None: + v_expr -= repn.nonlinear_expr + v_expr /= v_coef + v_con = c + v_repn = repn + v_vars = ComponentSet([i for i in v_repn.linear_vars if i in var_to_con_map]) + v_vars.update([i for i in v_repn.nonlinear_vars if i in var_to_con_map]) + v_vars.remove(v) + density = n_vars + if v_expr is None: + return False + + v_con.deactivate() + + if v.lb is not None or v.ub is not None: + new_con = bound_cons.add((v.lb, v_expr, v.ub)) + for _v in v_vars: + var_to_con_map[_v].add(new_con) + + for c in con_list: + if c is v_con: + continue + sub_map = {id(v): v_expr} + new_body = simplify_expr(replace_expressions(c.body, substitution_map=sub_map)) + c.set_value((c.lb, new_body, c.ub)) + for _v in v_vars: + var_to_con_map[_v].add(c) + + return True def is_relaxation(a: _BlockData, b: _BlockData, opt: appsi.base.Solver, feasibility_tol: float = 1e-6, bigM: Optional[float] = None): @@ -12,6 +84,20 @@ def is_relaxation(a: _BlockData, b: _BlockData, opt: appsi.base.Solver, feasibil (a is a relaxation of b) a and b should share variables """ + + """ + if a has variables that b does not, this will not work + see if we can presolve them out + Note - it is okay if b has variables that a does not + """ + a_vars = ComponentSet(active_vars(a)) + b_vars = ComponentSet(active_vars(b)) + vars_to_presolve = a_vars - b_vars + if len(vars_to_presolve) > 0: + a = clone_shallow_active_flat(a, clone_expressions=True)[0] + if not _attempt_presolve(a, vars_to_presolve): + raise RuntimeError('a has variables that b does not, which makes the following analysis invalid') + m = clone_shallow_active_flat(b)[0] if hasattr(m.linear, 'obj'): del m.linear.obj @@ -55,7 +141,9 @@ def is_relaxation(a: _BlockData, b: _BlockData, opt: appsi.base.Solver, feasibil res = opt.solve(m) assert res.termination_condition == appsi.base.TerminationCondition.optimal - return res.best_feasible_objective <= feasibility_tol + passed = res.best_feasible_objective <= feasibility_tol + + return passed def is_equivalent(a: _BlockData, b: _BlockData, opt: appsi.base.Solver, feasibility_tol: float = 1e-6, bigM: Optional[float] = None): diff --git a/pyomo/contrib/coramin/utils/pyomo_utils.py b/pyomo/contrib/coramin/utils/pyomo_utils.py index 7dac54a2df9..3c049375302 100644 --- a/pyomo/contrib/coramin/utils/pyomo_utils.py +++ b/pyomo/contrib/coramin/utils/pyomo_utils.py @@ -26,12 +26,6 @@ def get_objective(m): return obj -def unfixed_vars(m, descend_into=True, active=True): - for v in m.component_data_objects(pe.Var, descend_into=descend_into, active=active): - if not v.is_fixed(): - yield v - - def active_vars(m, include_fixed=False): seen = set() for c in m.component_data_objects(pe.Constraint, active=True, descend_into=True): @@ -49,6 +43,11 @@ def active_vars(m, include_fixed=False): yield v +def active_cons(m): + for c in m.component_data_objects(pe.Constraint, active=True, descend_into=True): + yield c + + simplifier = Simplifier() diff --git a/pyomo/contrib/coramin/utils/tests/test_compare_models.py b/pyomo/contrib/coramin/utils/tests/test_compare_models.py index 5c8dcecaad9..d1b9cd64b9c 100644 --- a/pyomo/contrib/coramin/utils/tests/test_compare_models.py +++ b/pyomo/contrib/coramin/utils/tests/test_compare_models.py @@ -1,7 +1,8 @@ import pyomo.environ as pe from pyomo.contrib import appsi from pyomo.common import unittest -from pyomo.contrib.coramin.utils.compare_models import is_relaxation, is_equivalent +from pyomo.contrib.coramin.utils.compare_models import is_relaxation, is_equivalent, _attempt_presolve +from pyomo.core.expr.compare import assertExpressionsEqual, compare_expressions class TestCompareModels(unittest.TestCase): @@ -23,3 +24,77 @@ def test_compare_models_1(self): self.assertFalse(is_equivalent(m1, m2, opt)) self.assertTrue(is_relaxation(m2, m1, opt)) self.assertFalse(is_relaxation(m1, m2, opt)) + + def _get_model(self): + m = pe.ConcreteModel() + m.x1 = pe.Var() + m.x2 = pe.Var() + m.x3 = pe.Var(bounds=(-3, 3)) + m.x4 = pe.Var() + m.c1 = pe.Constraint(expr=m.x1 + m.x2 + m.x3 + m.x4 == 1) + m.c2 = pe.Constraint(expr=m.x2 + m.x3 + m.x4 == 1) + m.c3 = pe.Constraint(expr=m.x3 + m.x4 == 1) + m.c4 = pe.Constraint(expr=m.x4 == 1) + return m + + def _compare_expressions(self, got, options): + success = False + for exp in options: + if compare_expressions(got, exp): + success = True + break + return success + + def test_presolve1(self): + m = self._get_model() + success = _attempt_presolve(m, [m.x3]) + self.assertTrue(success) + self.assertTrue(m.c1.active) + self.assertTrue(m.c2.active) + self.assertFalse(m.c3.active) + self.assertTrue(m.c4.active) + self.assertTrue(self._compare_expressions(m.c1.body, [m.x1 + m.x2 + 1, m.x2 + m.x1 + 1])) + self.assertTrue(self._compare_expressions(m.c2.body, [m.x2 + 1])) + self.assertEqual(m.c1.lb, 1) + self.assertEqual(m.c1.ub, 1) + self.assertEqual(m.c2.lb, 1) + self.assertEqual(m.c2.ub, 1) + self.assertEqual(len(m.bound_constraints), 1) + self.assertTrue(self._compare_expressions(m.bound_constraints[1].body, [1 - m.x4])) + self.assertEqual(m.bound_constraints[1].lb, -3) + self.assertEqual(m.bound_constraints[1].ub, 3) + + def test_presolve2(self): + m = self._get_model() + success = _attempt_presolve(m, [m.x3, m.x4]) + self.assertTrue(success) + self.assertTrue(m.c1.active) + self.assertTrue(m.c2.active) + self.assertFalse(m.c3.active) + self.assertFalse(m.c4.active) + self.assertTrue(self._compare_expressions(m.c1.body, [m.x1 + m.x2 + 1, m.x2 + m.x1 + 1])) + self.assertTrue(self._compare_expressions(m.c2.body, [m.x2 + 1])) + self.assertEqual(m.c1.lb, 1) + self.assertEqual(m.c1.ub, 1) + self.assertEqual(m.c2.lb, 1) + self.assertEqual(m.c2.ub, 1) + self.assertEqual(len(m.bound_constraints), 1) + self.assertEqual(m.bound_constraints[1].body, 0) + self.assertEqual(m.bound_constraints[1].lb, -3) + self.assertEqual(m.bound_constraints[1].ub, 3) + + def test_presolve3(self): + m = self._get_model() + success = _attempt_presolve(m, [m.x3, m.x4, m.x2]) + self.assertTrue(success) + self.assertTrue(m.c1.active) + self.assertFalse(m.c2.active) + self.assertFalse(m.c3.active) + self.assertFalse(m.c4.active) + self.assertTrue(self._compare_expressions(m.c1.body, [m.x1 + 1])) + self.assertEqual(m.c1.lb, 1) + self.assertEqual(m.c1.ub, 1) + self.assertEqual(len(m.bound_constraints), 1) + self.assertEqual(m.bound_constraints[1].body, 0) + self.assertEqual(m.bound_constraints[1].lb, -3) + self.assertEqual(m.bound_constraints[1].ub, 3) From 1d0e44d751b1bcca7a15607e5d467ad1b9b9a2db Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 31 Jan 2024 12:28:46 -0700 Subject: [PATCH 089/128] identify variables cache --- pyomo/contrib/coramin/utils/pyomo_utils.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/coramin/utils/pyomo_utils.py b/pyomo/contrib/coramin/utils/pyomo_utils.py index 3c049375302..9c39c5ef532 100644 --- a/pyomo/contrib/coramin/utils/pyomo_utils.py +++ b/pyomo/contrib/coramin/utils/pyomo_utils.py @@ -1,7 +1,9 @@ import pyomo.environ as pe from pyomo.core.expr.numvalue import is_fixed from pyomo.core.expr.visitor import identify_variables +from pyomo.core.base.constraint import _GeneralConstraintData from pyomo.contrib.simplification import Simplifier +from weakref import WeakKeyDictionary def get_objective(m): @@ -26,10 +28,25 @@ def get_objective(m): return obj +_var_cache = WeakKeyDictionary() + + +def identify_variables_with_cache(con: _GeneralConstraintData, include_fixed=False): + e = con.expr + if con in _var_cache and _var_cache[con][1] is e: + vlist = _var_cache[con][0] + else: + vlist = list(identify_variables(e, include_fixed=True)) + if not include_fixed: + vlist = [i for i in vlist if not i.fixed] + _var_cache[con] = (vlist, e) + return vlist + + def active_vars(m, include_fixed=False): seen = set() for c in m.component_data_objects(pe.Constraint, active=True, descend_into=True): - for v in identify_variables(c.body, include_fixed=include_fixed): + for v in identify_variables_with_cache(c, include_fixed=include_fixed): v_id = id(v) if v_id not in seen: seen.add(v_id) From ac48999063f89212463f5e595e3996ab44effd2b Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 31 Jan 2024 19:42:41 -0700 Subject: [PATCH 090/128] coramin: working on tests --- .../domain_reduction/tests/test_dbt.py | 29 +++++++++++++++++++ .../coramin/relaxations/copy_relaxation.py | 2 ++ 2 files changed, 31 insertions(+) diff --git a/pyomo/contrib/coramin/domain_reduction/tests/test_dbt.py b/pyomo/contrib/coramin/domain_reduction/tests/test_dbt.py index 5c4bfeeba86..85b592fee3e 100644 --- a/pyomo/contrib/coramin/domain_reduction/tests/test_dbt.py +++ b/pyomo/contrib/coramin/domain_reduction/tests/test_dbt.py @@ -102,6 +102,35 @@ def test_decomp2(self): self.assertEqual(m2.get_block_stage(b.children[1]), 2) self.assertEqual(len(b.coupling_vars), 1) + def test_decomp3(self): + m1 = pe.ConcreteModel() + m1.x = x = pe.Var([1, 2, 3, 4, 5, 6], bounds=(-10, 10)) + m1.c = c = pe.ConstraintList() + m1.rels = pe.Block() + + c.add(x[1] == x[2] + x[3]) + c.add(x[4] == x[5] + x[6]) + m1.rels.rel1 = coramin.relaxations.PWMcCormickRelaxation() + m1.rels.rel1.build(x[2], x[3], aux_var=x[1]) + m1.rels.rel2 = coramin.relaxations.PWMcCormickRelaxation() + m1.rels.rel2.build(x[5], x[6], aux_var=x[4]) + + m2, reason = decompose_model(m1) + self.assertEqual(reason, DecompositionStatus.normal) + self.assertTrue(is_equivalent(m1, m2, appsi.solvers.Highs())) + self.assertEqual(len(m2.children), 2) + self.assertEqual(len(list(coramin.relaxations.iterators.nonrelaxation_component_data_objects(m2.children[0], pe.Constraint, descend_into=True, active=True))), 1) + self.assertEqual(len(list(coramin.relaxations.iterators.nonrelaxation_component_data_objects(m2.children[1], pe.Constraint, descend_into=True, active=True))), 1) + self.assertEqual(len(list(coramin.relaxations.iterators.relaxation_data_objects(m2.children[0], descend_into=True, active=True))), 1) + self.assertEqual(len(list(coramin.relaxations.iterators.relaxation_data_objects(m2.children[1], descend_into=True, active=True))), 1) + self.assertEqual(len(list(active_vars(m2.children[0]))), 3) + self.assertEqual(len(list(active_vars(m2.children[1]))), 3) + self.assertEqual(m2.get_block_stage(m2), 0) + self.assertEqual(m2.get_block_stage(m2.children[0]), 1) + self.assertEqual(m2.get_block_stage(m2.children[1]), 1) + self.assertEqual(list(m2.stage_blocks(0)), [m2]) + self.assertEqual(list(m2.stage_blocks(1)), [m2.children[0], m2.children[1]]) + def test_refine_partition(self): m1 = pe.ConcreteModel() m1.x = x = pe.Var([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], bounds=(-10, 10)) diff --git a/pyomo/contrib/coramin/relaxations/copy_relaxation.py b/pyomo/contrib/coramin/relaxations/copy_relaxation.py index 01a09439620..98173a5d949 100644 --- a/pyomo/contrib/coramin/relaxations/copy_relaxation.py +++ b/pyomo/contrib/coramin/relaxations/copy_relaxation.py @@ -170,4 +170,6 @@ def copy_relaxation_with_local_data(rel, old_var_to_new_var_map=None): new_rel.large_coef = rel.large_coef new_rel.safety_tol = rel.safety_tol + new_rel.rebuild() + return new_rel From 38010163365c3d922f52a8db553373c394bff6f4 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 6 Feb 2024 20:28:00 -0700 Subject: [PATCH 091/128] update tests --- pyomo/contrib/coramin/clone.py | 6 +- pyomo/contrib/coramin/domain_reduction/dbt.py | 61 +-- .../domain_reduction/tests/test_dbt.py | 389 +++--------------- pyomo/contrib/coramin/utils/compare_models.py | 2 +- 4 files changed, 88 insertions(+), 370 deletions(-) diff --git a/pyomo/contrib/coramin/clone.py b/pyomo/contrib/coramin/clone.py index d0bb9516dfc..bf21e05a71f 100644 --- a/pyomo/contrib/coramin/clone.py +++ b/pyomo/contrib/coramin/clone.py @@ -35,7 +35,7 @@ def get_clone_and_var_map(m1: _BlockData): return m2, var_map -def clone_shallow_active_flat(m1: _BlockData, num_clones: int = 1, clone_expressions: bool = False) -> List[_BlockData]: +def clone_shallow_active_flat(m1: _BlockData, num_clones: int = 1) -> List[_BlockData]: clone_list = [pe.Block(concrete=True) for i in range(num_clones)] for m2 in clone_list: m2.linear = pe.Block() @@ -52,8 +52,6 @@ def clone_shallow_active_flat(m1: _BlockData, num_clones: int = 1, clone_express all_vars.update(repn.linear_vars) all_vars.update(repn.nonlinear_vars) body = repn.to_expression() - if clone_expressions: - body = body.clone() if repn.nonlinear_expr is None: for m2 in clone_list: m2.linear.cons.add((c.lb, body, c.ub)) @@ -68,8 +66,6 @@ def clone_shallow_active_flat(m1: _BlockData, num_clones: int = 1, clone_express all_vars.update(repn.linear_vars) all_vars.update(repn.nonlinear_vars) obj_expr = repn.to_expression() - if clone_expressions: - obj_expr = obj_expr.clone() if repn.nonlinear_expr is None: for m2 in clone_list: m2.linear.obj = pe.Objective(expr=obj_expr, sense=obj.sense) diff --git a/pyomo/contrib/coramin/domain_reduction/dbt.py b/pyomo/contrib/coramin/domain_reduction/dbt.py index 3ed82f1198d..7abcd668aeb 100644 --- a/pyomo/contrib/coramin/domain_reduction/dbt.py +++ b/pyomo/contrib/coramin/domain_reduction/dbt.py @@ -61,12 +61,16 @@ def __init__(self, component): _BlockData.__init__(self, component) self._children_index = None self._children = None - self._coupling_vars = list() + self.coupling_vars = list() self._already_setup = False self._is_leaf = None self._is_root = True - def setup(self, children_keys, coupling_vars): + def is_root(self): + self._assert_setup() + return self._is_root + + def setup(self, children_keys, coupling_vars=None): assert not self._already_setup self._already_setup = True if len(children_keys) == 0: @@ -75,10 +79,12 @@ def setup(self, children_keys, coupling_vars): self._is_leaf = False del self._children_index del self._children - del self._coupling_vars self._children_index = pe.Set(initialize=children_keys) self._children = TreeBlock(self._children_index) - self._coupling_vars = list(coupling_vars) + if coupling_vars is None: + self.coupling_vars = list() + else: + self.coupling_vars = list(coupling_vars) for key in children_keys: child = self.children[key] child._is_root = False @@ -101,15 +107,6 @@ def children(self): 'Leaf TreeBlocks do not have children. Please check the is_leaf method' ) return self._children - - @property - def coupling_vars(self): - self._assert_setup() - if self.is_leaf(): - raise TreeBlockError( - 'Leaf TreeBlocks do not have coupling variables. Please check the is_leaf method' - ) - return list(self._coupling_vars) def _num_stages(self): self._assert_setup() @@ -719,14 +716,16 @@ def _reformulate_objective(model): lb, ub = compute_bounds_on_expr(current_obj.expr) obj_var.setlb(lb) obj_var.setub(ub) - model.del_component(current_obj) - new_objective = pe.Objective(expr=model.obj_var) + current_obj_expr = current_obj.expr + current_obj_sense = current_obj.sense + current_obj.parent_block().del_component(current_obj) + new_objective = pe.Objective(expr=obj_var) new_obj_name = unique_component_name(model, 'objective') model.add_component(new_obj_name, new_objective) - if current_obj.sense == pe.minimize: - obj_con = pe.Constraint(expr=current_obj.expr <= obj_var) + if current_obj_sense == pe.minimize: + obj_con = pe.Constraint(expr=current_obj_expr <= obj_var) else: - obj_con = pe.Constraint(expr=current_obj.expr >= obj_var) + obj_con = pe.Constraint(expr=current_obj_expr >= obj_var) new_objective.sense = pe.maximize obj_con_name = unique_component_name(model, 'obj_con') model.add_component(obj_con_name, obj_con) @@ -761,13 +760,13 @@ def _decompose_model( termination_reason: DecompositionStatus An enum member from DecompositionStatus """ + new_model = TreeBlock(concrete=True) + new_model.aux_vars = pe.VarList() + object.__setattr__(model, 'aux_vars', new_model.aux_vars) # by reformulating the objective, we can make better use of the incumbent when # doing OBBT _reformulate_objective(model) - new_model = TreeBlock(concrete=True) - new_model.aux_vars = pe.VarList() - object.__setattr__(model, 'aux_vars', new_model.aux_vars) graph = convert_pyomo_model_to_bipartite_graph(model) logger.debug('converted pyomo model to bipartite graph') @@ -1255,7 +1254,10 @@ def perform_dbt( for stage in range(num_stages): stage_blocks = list(relaxation.stage_blocks(stage)) for block in stage_blocks: - vars_to_tighten = vars_to_tighten_by_block[block] + if block in vars_to_tighten_by_block: + vars_to_tighten = vars_to_tighten_by_block[block] + else: + vars_to_tighten = list() if obbt_method == OBBTMethod.FULL_SPACE or block.is_leaf(): dbt_info.num_vars_to_tighten += 2 * len(vars_to_tighten) else: @@ -1302,9 +1304,9 @@ def perform_dbt( if time.time() - t0 >= time_limit: break - if obbt_method in {OBBTMethod.LEAVES, OBBTMethod.FULL_SPACE} and ( - not block.is_leaf() - ): + if obbt_method == OBBTMethod.LEAVES and not block.is_leaf(): + continue + if obbt_method == OBBTMethod.FULL_SPACE and not block.is_root(): continue if obbt_method == OBBTMethod.FULL_SPACE: block_to_tighten_with = relaxation @@ -1316,7 +1318,10 @@ def perform_dbt( else: _ub = None - vars_to_tighten = vars_to_tighten_by_block[block] + if block in vars_to_tighten_by_block: + vars_to_tighten = vars_to_tighten_by_block[block] + else: + vars_to_tighten = list() if filter_method == FilterMethod.AGGRESSIVE: logger.debug('starting filter') @@ -1436,10 +1441,6 @@ def perform_dbt( logger.debug('done tightening ubs') - if not block.is_leaf(): - for c in block.linking_constraints.values(): - fbbt(c) - return dbt_info diff --git a/pyomo/contrib/coramin/domain_reduction/tests/test_dbt.py b/pyomo/contrib/coramin/domain_reduction/tests/test_dbt.py index 85b592fee3e..a58375167e6 100644 --- a/pyomo/contrib/coramin/domain_reduction/tests/test_dbt.py +++ b/pyomo/contrib/coramin/domain_reduction/tests/test_dbt.py @@ -13,6 +13,7 @@ OBBTMethod, FilterMethod, DecompositionStatus, + compute_partition_ratio, ) from pyomo.common import unittest import pyomo.environ as pe @@ -130,6 +131,36 @@ def test_decomp3(self): self.assertEqual(m2.get_block_stage(m2.children[1]), 1) self.assertEqual(list(m2.stage_blocks(0)), [m2]) self.assertEqual(list(m2.stage_blocks(1)), [m2.children[0], m2.children[1]]) + pr = compute_partition_ratio(m1, m2) + self.assertAlmostEqual(pr, 2) + + def test_objective(self): + m1 = pe.ConcreteModel() + m1.x = x = pe.Var([1, 2, 3, 4, 5, 6], bounds=(-10, 10)) + m1.c = c = pe.ConstraintList() + + c.add(x[1] == x[2] + x[3]) + c.add(x[4] == x[5] + x[6]) + c.add(x[2] <= 2*x[3] + 1) + c.add(x[5] >= 2*x[6] + 1) + m1.obj = pe.Objective(expr=sum(x.values())) + + m2, reason = decompose_model(m1) + self.assertEqual(reason, DecompositionStatus.normal) + opt = appsi.solvers.Highs() + res1 = opt.solve(m1) + res2 = opt.solve(m2) + self.assertAlmostEqual(res1.best_feasible_objective, res2.best_feasible_objective) + self.assertEqual(len(m2.children), 2) + self.assertIn(len(list(active_cons(m2.children[0]))), {3, 4}) + self.assertIn(len(list(active_cons(m2.children[1]))), {3, 4}) + self.assertIn(len(list(active_vars(m2.children[0]))), {4, 5}) + self.assertIn(len(list(active_vars(m2.children[1]))), {4, 5}) + self.assertEqual(m2.get_block_stage(m2), 0) + self.assertEqual(m2.get_block_stage(m2.children[0]), 1) + self.assertEqual(m2.get_block_stage(m2.children[1]), 1) + self.assertEqual(list(m2.stage_blocks(0)), [m2]) + self.assertEqual(list(m2.stage_blocks(1)), [m2.children[0], m2.children[1]]) def test_refine_partition(self): m1 = pe.ConcreteModel() @@ -150,7 +181,6 @@ def test_refine_partition(self): c.add(sum(x.values()) == 1) m2, reason = decompose_model(m1) - m2.pprint() self.assertEqual(reason, DecompositionStatus.normal) opt = appsi.solvers.Highs() opt.config.stream_solver = True @@ -173,8 +203,6 @@ def test_tree_block(self): b.is_leaf() with self.assertRaises(TreeBlockError): b.children - with self.assertRaises(TreeBlockError): - b.coupling_vars with self.assertRaises(TreeBlockError): b.num_stages() with self.assertRaises(TreeBlockError): @@ -185,8 +213,6 @@ def test_tree_block(self): self.assertTrue(b.is_leaf()) b.x = pe.Var() # make sure we can add components just like a regular block b.x.setlb(-1) - with self.assertRaises(TreeBlockError): - b.coupling_vars with self.assertRaises(TreeBlockError): b.children self.assertEqual(b.num_stages(), 1) @@ -422,12 +448,8 @@ def test_split_metis(self): self.assertEqual(v4, cv) new_model = TreeBlock(concrete=True) - component_map = tree.build_pyomo_model(block=new_model) - new_vars = list( - coramin.relaxations.nonrelaxation_component_data_objects( - new_model, ctype=pe.Var, descend_into=True, sort=True - ) - ) + tree.build_pyomo_model(block=new_model) + new_vars = list(active_vars(new_model)) new_cons = list( coramin.relaxations.nonrelaxation_component_data_objects( new_model, @@ -442,11 +464,11 @@ def test_split_metis(self): new_model, descend_into=True, active=True, sort=True ) ) - self.assertEqual(len(new_vars), 7) - self.assertEqual(len(new_cons), 3) + self.assertEqual(len(new_vars), 6) + self.assertEqual(len(new_cons), 2) self.assertEqual(len(new_rels), 2) self.assertEqual(len(new_model.children), 2) - self.assertEqual(len(new_model.linking_constraints), 1) + self.assertEqual(len(new_model.coupling_vars), 1) self.assertEqual(new_model.num_stages(), 2) stage0_vars = list( @@ -463,21 +485,13 @@ def test_split_metis(self): ) ) self.assertEqual(len(stage0_vars), 0) - self.assertEqual(len(stage0_cons), 1) + self.assertEqual(len(stage0_cons), 0) self.assertEqual(len(stage0_rels), 0) block_a = new_model.children[0] block_b = new_model.children[1] - block_a_vars = ComponentSet( - coramin.relaxations.nonrelaxation_component_data_objects( - block_a, ctype=pe.Var, descend_into=True, sort=True - ) - ) - block_b_vars = ComponentSet( - coramin.relaxations.nonrelaxation_component_data_objects( - block_b, ctype=pe.Var, descend_into=True, sort=True - ) - ) + block_a_vars = ComponentSet(active_vars(block_a)) + block_b_vars = ComponentSet(active_vars(block_b)) block_a_cons = ComponentSet( coramin.relaxations.nonrelaxation_component_data_objects( block_a, ctype=pe.Constraint, descend_into=True, active=True, sort=True @@ -498,99 +512,15 @@ def test_split_metis(self): block_b, descend_into=True, active=True, sort=True ) ) - if component_map[m.v1] not in block_a_vars: - block_a, block_b = block_b, block_a - block_a_vars, block_b_vars = block_b_vars, block_a_vars - block_a_cons, block_b_cons = block_b_cons, block_a_cons - block_a_rels, block_b_rels = block_b_rels, block_a_rels - self.assertEqual(len(block_a_vars), 4) + self.assertIn(len(block_a_vars), {3, 4}) self.assertEqual(len(block_a_cons), 1) self.assertEqual(len(block_a_rels), 1) - self.assertEqual(len(block_b_vars), 3) + self.assertIn(len(block_b_vars), {3, 4}) self.assertEqual(len(block_b_cons), 1) self.assertEqual(len(block_b_rels), 1) - v1 = component_map[m.v1] - v2 = component_map[m.v2] - v3 = component_map[m.v3] - v4_a = block_a.vars['v4'] - v4_b = block_b.vars['v4'] - v5 = component_map[m.v5] - v6 = component_map[m.v6] - - self.assertIs(v1, block_a.vars['v1']) - self.assertIs(v2, block_a.vars['v2']) - self.assertIs(v3, block_a.vars['v3']) - self.assertIs(v5, block_b.vars['v5']) - self.assertIs(v6, block_b.vars['v6']) - - self.assertEqual(v2.lb, -1) - self.assertEqual(v2.ub, 1) - self.assertEqual(v3.lb, -1) - self.assertEqual(v3.ub, 1) - self.assertEqual(v4_a.lb, -1) - self.assertEqual(v4_a.ub, 1) - self.assertEqual(v4_b.lb, -1) - self.assertEqual(v4_b.ub, 1) - self.assertEqual(v5.lb, -1) - self.assertEqual(v5.ub, 1) - self.assertEqual(v1.lb, None) - self.assertEqual(v1.ub, None) - self.assertEqual(v6.lb, None) - self.assertEqual(v6.ub, None) - - linking_con = new_model.linking_constraints[1] - linking_con_vars = ComponentSet(identify_variables(linking_con.body)) - self.assertEqual(len(linking_con_vars), 2) - self.assertIn(v4_a, linking_con_vars) - self.assertIn(v4_b, linking_con_vars) - derivs = differentiate( - expr=linking_con.body, mode=differentiate.Modes.reverse_symbolic - ) - self.assertTrue( - (derivs[v4_a] == 1 and derivs[v4_b] == -1) - or (derivs[v4_a] == -1 and derivs[v4_b] == 1) - ) - self.assertEqual(linking_con.lower, 0) - self.assertEqual(linking_con.upper, 0) - - c1 = block_a.cons['c1'] - c2 = block_b.cons['c2'] - r1 = block_b.rels.r1 - r2 = block_a.rels.r2 - c1_vars = ComponentSet(identify_variables(c1.body)) - c2_vars = ComponentSet(identify_variables(c2.body)) - self.assertEqual(len(c1_vars), 3) - self.assertEqual(len(c2_vars), 3) - self.assertIn(v1, c1_vars) - self.assertIn(v2, c1_vars) - self.assertIn(v3, c1_vars) - self.assertIn(v4_b, c2_vars) - self.assertIn(v5, c2_vars) - self.assertIn(v6, c2_vars) - self.assertIs(r1.get_aux_var(), v6) - self.assertIs(r2.get_aux_var(), v2) - r1_rhs_vars = ComponentSet(r1.get_rhs_vars()) - r2_rhs_vars = ComponentSet(r2.get_rhs_vars()) - self.assertIn(v3, r2_rhs_vars) - self.assertIn(v4_a, r2_rhs_vars) - self.assertIn(v4_b, r1_rhs_vars) - self.assertIn(v5, r1_rhs_vars) - self.assertTrue(isinstance(r1, coramin.relaxations.PWMcCormickRelaxationData)) - self.assertTrue(isinstance(r2, coramin.relaxations.PWMcCormickRelaxationData)) - c1_derivs = differentiate(c1.body, mode=differentiate.Modes.reverse_symbolic) - c2_derivs = differentiate(c2.body, mode=differentiate.Modes.reverse_symbolic) - self.assertEqual(c1_derivs[v1], 1) - self.assertEqual(c1_derivs[v2], -1) - self.assertEqual(c1_derivs[v3], -1) - self.assertEqual(c2_derivs[v4_b], -1) - self.assertEqual(c2_derivs[v5], -1) - self.assertEqual(c2_derivs[v6], 1) - self.assertEqual(c1.lower, 0) - self.assertEqual(c1.upper, 0) - self.assertEqual(c2.lower, 0) - self.assertEqual(c2.upper, 0) + self.assertEqual(new_model.coupling_vars, [m.v4]) class TestNumCons(unittest.TestCase): @@ -612,198 +542,6 @@ def test_num_cons(self): self.assertEqual(num_cons_in_graph(g), 2) -class TestDecompose(unittest.TestCase): - def helper(self, case, min_partition_ratio, expected_termination): - """ - we rely on other tests to make sure the relaxation is constructed - correctly. This test just checks the decomposition. - """ - - test_dir = os.path.dirname(os.path.abspath(__file__)) - pglib_dir = os.path.join(test_dir, 'pglib-opf-master') - if not os.path.isdir(pglib_dir): - get_pglib_opf(download_dir=test_dir) - md = ModelData.read(filename=os.path.join(pglib_dir, case)) - m, scaled_md = create_psv_acopf_model(md) - opt = pe.SolverFactory('ipopt') - res = opt.solve(m, tee=False) - - relaxed_m = coramin.relaxations.relax( - m, - in_place=False, - use_fbbt=False, - fbbt_options={'deactivate_satisfied_constraints': True, 'max_iter': 2}, - use_alpha_bb=False, - ) - (decomposed_m, component_map, termination_reason) = decompose_model( - model=relaxed_m, - max_leaf_nnz=1000, - min_partition_ratio=1.4, - limit_num_stages=True, - ) - self.assertEqual(termination_reason, expected_termination) - if ( - expected_termination - == coramin.domain_reduction.dbt.DecompositionStatus.normal - ): - self.assertGreaterEqual(decomposed_m.num_stages(), 2) - - for r in coramin.relaxations.relaxation_data_objects( - block=relaxed_m, descend_into=True, active=True, sort=True - ): - r.rebuild(build_nonlinear_constraint=True) - for r in coramin.relaxations.relaxation_data_objects( - block=decomposed_m, descend_into=True, active=True, sort=True - ): - r.rebuild(build_nonlinear_constraint=True) - relaxed_res = opt.solve(relaxed_m, tee=False) - decomposed_res = opt.solve(decomposed_m, tee=False) - - self.assertEqual( - res.solver.termination_condition, pe.TerminationCondition.optimal - ) - self.assertEqual( - relaxed_res.solver.termination_condition, pe.TerminationCondition.optimal - ) - self.assertEqual( - decomposed_res.solver.termination_condition, pe.TerminationCondition.optimal - ) - obj = get_objective(m) - relaxed_obj = get_objective(relaxed_m) - decomposed_obj = get_objective(decomposed_m) - val = pe.value(obj.expr) - relaxed_val = pe.value(relaxed_obj.expr) - decomposed_val = pe.value(decomposed_obj.expr) - relaxed_rel_diff = abs(val - relaxed_val) / val - decomposed_rel_diff = abs(val - decomposed_val) / val - self.assertAlmostEqual(relaxed_rel_diff, 0, 5) - self.assertAlmostEqual(decomposed_rel_diff, 0, 5) - - relaxed_vars = list( - coramin.relaxations.nonrelaxation_component_data_objects( - relaxed_m, pe.Var, sort=True, descend_into=True - ) - ) - relaxed_vars = [v for v in relaxed_vars if not v.fixed] - relaxed_cons = list( - coramin.relaxations.nonrelaxation_component_data_objects( - relaxed_m, pe.Constraint, active=True, sort=True, descend_into=True - ) - ) - relaxed_rels = list( - coramin.relaxations.relaxation_data_objects( - relaxed_m, descend_into=True, active=True, sort=True - ) - ) - decomposed_vars = list( - coramin.relaxations.nonrelaxation_component_data_objects( - decomposed_m, pe.Var, sort=True, descend_into=True - ) - ) - decomposed_cons = list( - coramin.relaxations.nonrelaxation_component_data_objects( - decomposed_m, pe.Constraint, active=True, sort=True, descend_into=True - ) - ) - decomposed_rels = list( - coramin.relaxations.relaxation_data_objects( - decomposed_m, descend_into=True, active=True, sort=True - ) - ) - linking_cons = list() - for stage in range(decomposed_m.num_stages()): - for block in decomposed_m.stage_blocks(stage): - if not block.is_leaf(): - linking_cons.extend(block.linking_constraints.values()) - relaxed_vars_mapped = list() - for i in relaxed_vars: - relaxed_vars_mapped.append(component_map[i]) - relaxed_vars_mapped = ComponentSet(relaxed_vars_mapped) - var_diff = ComponentSet(decomposed_vars) - relaxed_vars_mapped - extra_vars = ComponentSet() - for c in linking_cons: - for v in identify_variables(c.body, include_fixed=True): - extra_vars.add(v) - for v in coramin.relaxations.nonrelaxation_component_data_objects( - decomposed_m, pe.Var, descend_into=True - ): - if 'dbt_partition_vars' in str(v) or 'obj_var' in str(v): - extra_vars.add(v) - extra_vars = extra_vars - relaxed_vars_mapped - partition_cons = ComponentSet() - obj_cons = ComponentSet() - for c in coramin.relaxations.nonrelaxation_component_data_objects( - decomposed_m, pe.Constraint, active=True, descend_into=True - ): - if 'dbt_partition_cons' in str(c): - partition_cons.add(c) - elif 'obj_con' in str(c): - obj_cons.add(c) - for v in var_diff: - self.assertIn(v, extra_vars) - var_diff = relaxed_vars_mapped - ComponentSet(decomposed_vars) - self.assertEqual(len(var_diff), 0) - self.assertEqual(len(relaxed_vars) + len(extra_vars), len(decomposed_vars)) - - rcs = list() - for i in relaxed_cons + linking_cons + list(partition_cons) + list(obj_cons): - rcs.append(str(i)) - dcs = [str(i) for i in decomposed_cons] - - def _reformat(s: str) -> str: - s = s.split('.cons') - if len(s) > 1: - s = s[1] - s = s.lstrip('[') - s = s.rstrip(']') - elif s[0].startswith('cons'): - s = s[0] - s = s.lstrip('cons') - s = s.lstrip('[') - s = s.rstrip(']') - else: - s = s[0] - s = s.replace('"', '') - s = s.replace("'", "") - return s - - rcs = set([_reformat(i) for i in rcs]) - dcs = set([_reformat(i) for i in dcs]) - - self.assertEqual(rcs, dcs) - - # self.assertEqual(len(relaxed_cons) + len(linking_cons) + len(partition_cons) - len(partition_cons)/3 + len(obj_cons), len(decomposed_cons)) - self.assertEqual(len(relaxed_rels), len(decomposed_rels)) - - def test_decompose1(self): - self.helper( - 'pglib_opf_case5_pjm.m', - min_partition_ratio=1.5, - expected_termination=coramin.domain_reduction.dbt.DecompositionStatus.problem_too_small, - ) - - def test_decompose2(self): - self.helper( - 'pglib_opf_case30_ieee.m', - min_partition_ratio=1.5, - expected_termination=coramin.domain_reduction.dbt.DecompositionStatus.normal, - ) - - def test_decompose3(self): - self.helper( - 'pglib_opf_case118_ieee.m', - min_partition_ratio=1.5, - expected_termination=coramin.domain_reduction.dbt.DecompositionStatus.normal, - ) - - def test_decompose4(self): - self.helper( - 'pglib_opf_case14_ieee.m', - min_partition_ratio=1.4, - expected_termination=coramin.domain_reduction.dbt.DecompositionStatus.normal, - ) - - class TestVarsToTightenByBlock(unittest.TestCase): def test_vars_to_tighten_by_block(self): m = TreeBlock(concrete=True) @@ -820,11 +558,10 @@ def test_vars_to_tighten_by_block(self): b2.x = pe.Var(bounds=(-1, 1)) b2.y = pe.Var() - b2.z = pe.Var() b2.aux = pe.Var() b1.c = pe.Constraint(expr=b1.x + b1.y + b1.z == 0) - b2.c = pe.Constraint(expr=b2.x + b2.y + b2.z == 0) + b2.c = pe.Constraint(expr=b2.x + b2.y + b1.z == 0) b1.r = coramin.relaxations.PWUnivariateRelaxation() b1.r.set_input( @@ -841,43 +578,36 @@ def test_vars_to_tighten_by_block(self): ) b2.r.rebuild() - m.linking_constraints.add(b1.z == b2.z) + m.coupling_vars.append(b1.z) vars_to_tighten_by_block = collect_vars_to_tighten_by_block( m, method='full_space' ) - self.assertEqual(len(vars_to_tighten_by_block), 3) + self.assertEqual(len(vars_to_tighten_by_block), 1) vars_to_tighten = vars_to_tighten_by_block[m] - self.assertEqual(len(vars_to_tighten), 0) - vars_to_tighten = vars_to_tighten_by_block[b1] self.assertEqual(len(vars_to_tighten), 1) self.assertIn(b1.x, vars_to_tighten) - vars_to_tighten = vars_to_tighten_by_block[b2] - self.assertEqual(len(vars_to_tighten), 1) - self.assertIn(b2.x, vars_to_tighten) vars_to_tighten_by_block = collect_vars_to_tighten_by_block(m, method='leaves') - self.assertEqual(len(vars_to_tighten_by_block), 3) + self.assertIn(len(vars_to_tighten_by_block), {2, 3}) vars_to_tighten = vars_to_tighten_by_block[m] self.assertEqual(len(vars_to_tighten), 0) vars_to_tighten = vars_to_tighten_by_block[b1] self.assertEqual(len(vars_to_tighten), 1) self.assertIn(b1.x, vars_to_tighten) vars_to_tighten = vars_to_tighten_by_block[b2] - self.assertEqual(len(vars_to_tighten), 1) - self.assertIn(b2.x, vars_to_tighten) + self.assertEqual(len(vars_to_tighten), 0) vars_to_tighten_by_block = collect_vars_to_tighten_by_block(m, method='dbt') self.assertEqual(len(vars_to_tighten_by_block), 3) vars_to_tighten = vars_to_tighten_by_block[m] self.assertEqual(len(vars_to_tighten), 1) - self.assertTrue(b1.z in vars_to_tighten or b2.z in vars_to_tighten) + self.assertTrue(b1.z in vars_to_tighten) vars_to_tighten = vars_to_tighten_by_block[b1] self.assertEqual(len(vars_to_tighten), 1) self.assertIn(b1.x, vars_to_tighten) vars_to_tighten = vars_to_tighten_by_block[b2] - self.assertEqual(len(vars_to_tighten), 1) - self.assertIn(b2.x, vars_to_tighten) + self.assertEqual(len(vars_to_tighten), 0) class TestDBT(unittest.TestCase): @@ -901,17 +631,16 @@ def get_model(self): ) b1.x = pe.Var(bounds=(-5, 5)) - b1.y = pe.Var(bounds=(-5, 5)) b1.p = pe.Param(initialize=1.0, mutable=True) b1.c = coramin.relaxations.PWUnivariateRelaxation() b1.c.build( x=b1.x, - aux_var=b1.y, + aux_var=b0.y, shape=coramin.utils.FunctionShape.CONVEX, f_x_expr=b1.p * b1.x, ) - m.linking_constraints.add(b0.y == b1.y) + m.coupling_vars.append(b0.y) return m @@ -919,7 +648,7 @@ def test_full_space(self): m = self.get_model() b0 = m.children[0] b1 = m.children[1] - opt = appsi.solvers.Gurobi() + opt = appsi.solvers.Highs() perform_dbt( relaxation=m, solver=opt, @@ -932,8 +661,6 @@ def test_full_space(self): self.assertAlmostEqual(b0.y.ub, 5) self.assertAlmostEqual(b1.x.lb, -1) self.assertAlmostEqual(b1.x.ub, 1) - self.assertAlmostEqual(b1.y.lb, -5) - self.assertAlmostEqual(b1.y.ub, 5) def test_leaves(self): m = self.get_model() @@ -952,14 +679,12 @@ def test_leaves(self): self.assertAlmostEqual(b0.y.ub, 5) self.assertAlmostEqual(b1.x.lb, -5) self.assertAlmostEqual(b1.x.ub, 5) - self.assertAlmostEqual(b1.y.lb, -5) - self.assertAlmostEqual(b1.y.ub, 5) def test_dbt(self): m = self.get_model() b0 = m.children[0] b1 = m.children[1] - opt = appsi.solvers.Gurobi() + opt = appsi.solvers.Highs() perform_dbt( relaxation=m, solver=opt, @@ -972,14 +697,12 @@ def test_dbt(self): self.assertAlmostEqual(b0.y.ub, 1) self.assertAlmostEqual(b1.x.lb, -1) self.assertAlmostEqual(b1.x.ub, 1) - self.assertAlmostEqual(b1.y.lb, -1) - self.assertAlmostEqual(b1.y.ub, 1) def test_dbt_with_filter(self): m = self.get_model() b0 = m.children[0] b1 = m.children[1] - opt = appsi.solvers.Gurobi() + opt = appsi.solvers.Highs() perform_dbt( relaxation=m, solver=opt, @@ -992,9 +715,7 @@ def test_dbt_with_filter(self): self.assertAlmostEqual(b0.y.ub, 1) self.assertAlmostEqual(b1.x.lb, -1) self.assertAlmostEqual(b1.x.ub, 1) - self.assertAlmostEqual(b1.y.lb, -1) - self.assertAlmostEqual(b1.y.ub, 1) - + class TestDBTWithECP(unittest.TestCase): def create_model(self): diff --git a/pyomo/contrib/coramin/utils/compare_models.py b/pyomo/contrib/coramin/utils/compare_models.py index b47b0951b0b..71e6395b23b 100644 --- a/pyomo/contrib/coramin/utils/compare_models.py +++ b/pyomo/contrib/coramin/utils/compare_models.py @@ -94,7 +94,7 @@ def is_relaxation(a: _BlockData, b: _BlockData, opt: appsi.base.Solver, feasibil b_vars = ComponentSet(active_vars(b)) vars_to_presolve = a_vars - b_vars if len(vars_to_presolve) > 0: - a = clone_shallow_active_flat(a, clone_expressions=True)[0] + a = clone_shallow_active_flat(a)[0] if not _attempt_presolve(a, vars_to_presolve): raise RuntimeError('a has variables that b does not, which makes the following analysis invalid') From 6c4d827aea2309722a01ad3e2d3ea322f4ddc367 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 6 Feb 2024 20:40:01 -0700 Subject: [PATCH 092/128] update tests --- .../coramin/domain_reduction/tests/test_filters.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/pyomo/contrib/coramin/domain_reduction/tests/test_filters.py b/pyomo/contrib/coramin/domain_reduction/tests/test_filters.py index 56ea31686f3..69cadda292a 100644 --- a/pyomo/contrib/coramin/domain_reduction/tests/test_filters.py +++ b/pyomo/contrib/coramin/domain_reduction/tests/test_filters.py @@ -11,9 +11,9 @@ def test_basic_filter(self): m.x = pe.Var(bounds=(-2, -1)) m.obj = pe.Objective(expr=m.y) m.c = pe.Constraint(expr=m.y == -m.x**2) - coramin.relaxations.relax(m, in_place=True) - opt = appsi.solvers.Gurobi() - res = opt.solve(m) + r = coramin.relaxations.relax(m) + opt = appsi.solvers.Highs() + res = opt.solve(r) ( vars_to_min, vars_to_max, @@ -27,10 +27,10 @@ def test_aggressive_filter(self): m.x = pe.Var(bounds=(-2, -1)) m.obj = pe.Objective(expr=m.y) m.c = pe.Constraint(expr=m.y == -m.x**2) - coramin.relaxations.relax(m, in_place=True) - opt = appsi.solvers.Gurobi() + r = coramin.relaxations.relax(m) + opt = appsi.solvers.Highs() vars_to_min, vars_to_max = coramin.domain_reduction.aggressive_filter( - candidate_variables=[m.x], relaxation=m, solver=opt + candidate_variables=[m.x], relaxation=r, solver=opt ) self.assertNotIn(m.x, vars_to_max) self.assertNotIn(m.x, vars_to_min) From 26df9b5dda515cda6246be20e1c1bb3e109a123f Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 6 Feb 2024 21:23:10 -0700 Subject: [PATCH 093/128] coramin: working on tests --- .coveragerc | 2 ++ .../coramin/heuristics/tests/__init__.py | 0 .../heuristics/tests/test_heuristics.py | 24 +++++++++++++++++++ 3 files changed, 26 insertions(+) create mode 100644 pyomo/contrib/coramin/heuristics/tests/__init__.py create mode 100644 pyomo/contrib/coramin/heuristics/tests/test_heuristics.py diff --git a/.coveragerc b/.coveragerc index 5be40925509..412c4c33008 100644 --- a/.coveragerc +++ b/.coveragerc @@ -3,6 +3,7 @@ omit = setup.py */tests/* */tmp/* + */minlplib/* # The [run] section must be at the end, as the build harness will add a # "data_file" directive to the end of this file. @@ -17,3 +18,4 @@ source = omit = # github actions creates a cache directory we don't want measured cache/* + */minlplib/* diff --git a/pyomo/contrib/coramin/heuristics/tests/__init__.py b/pyomo/contrib/coramin/heuristics/tests/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/pyomo/contrib/coramin/heuristics/tests/test_heuristics.py b/pyomo/contrib/coramin/heuristics/tests/test_heuristics.py new file mode 100644 index 00000000000..050740d30e4 --- /dev/null +++ b/pyomo/contrib/coramin/heuristics/tests/test_heuristics.py @@ -0,0 +1,24 @@ +from pyomo.common import unittest +import pyomo.environ as pe +from pyomo.contrib.coramin.heuristics.diving import run_diving_heuristic + + +class TestDiving(unittest.TestCase): + def test_diving_1(self): + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.z1 = pe.Var(domain=pe.Binary) + m.z2 = pe.Var(domain=pe.Binary) + m.obj = pe.Objective(expr=m.x**2 + m.y**2) + m.c1 = pe.Constraint(expr=(m.y - pe.exp(m.x)) * m.z1 >= 0) + m.c2 = pe.Constraint(expr=(m.y - (m.x - 1)**2) * m.z1 >= 0) + m.c3 = pe.Constraint(expr=(m.y - m.x - 2) * m.z2 >= 0) + m.c4 = pe.Constraint(expr=(m.y + m.x - 2) * m.z2 >= 0) + m.c5 = pe.Constraint(expr=m.z1 + m.z2 == 1) + obj, sol = run_diving_heuristic(m) + self.assertAlmostEqual(obj, 1) + self.assertAlmostEqual(sol[m.x], 0) + self.assertAlmostEqual(sol[m.y], 1) + self.assertAlmostEqual(sol[m.z1], 1) + self.assertAlmostEqual(sol[m.z2], 0) From acb2f9deb5319d490e647759eae6857c91502424 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 7 Feb 2024 23:11:22 -0700 Subject: [PATCH 094/128] coramin: working on tests --- pyomo/contrib/coramin/algorithms/__init__.py | 1 + .../coramin/algorithms/bnb/__init__.py | 0 pyomo/contrib/coramin/algorithms/bnb/bnb.py | 35 +- .../coramin/algorithms/bnb/tests/__init__.py | 0 .../coramin/algorithms/bnb/tests/test_bnb.py | 317 ++++++++++++++++++ 5 files changed, 347 insertions(+), 6 deletions(-) create mode 100644 pyomo/contrib/coramin/algorithms/bnb/__init__.py create mode 100644 pyomo/contrib/coramin/algorithms/bnb/tests/__init__.py create mode 100644 pyomo/contrib/coramin/algorithms/bnb/tests/test_bnb.py diff --git a/pyomo/contrib/coramin/algorithms/__init__.py b/pyomo/contrib/coramin/algorithms/__init__.py index 14e97b59fa7..4703cfef89f 100644 --- a/pyomo/contrib/coramin/algorithms/__init__.py +++ b/pyomo/contrib/coramin/algorithms/__init__.py @@ -1,2 +1,3 @@ from .ecp_bounder import ECPBounder from .multitree.multitree import MultiTree +from .bnb.bnb import BnBSolver diff --git a/pyomo/contrib/coramin/algorithms/bnb/__init__.py b/pyomo/contrib/coramin/algorithms/bnb/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/pyomo/contrib/coramin/algorithms/bnb/bnb.py b/pyomo/contrib/coramin/algorithms/bnb/bnb.py index e771152f1aa..33ca70676ec 100644 --- a/pyomo/contrib/coramin/algorithms/bnb/bnb.py +++ b/pyomo/contrib/coramin/algorithms/bnb/bnb.py @@ -261,7 +261,12 @@ def bound(self): sol = np.array([v.value for v in self.all_vars], dtype=float) self.current_node.state.sol = sol self.current_node.state.obj = res.best_feasible_objective - return res.best_feasible_objective + ret = res.best_feasible_objective + if self.sense() == pybnb.minimize: + ret -= min(abs(ret) * 0.001 * self.config.mip_gap, 0.01) + else: + ret += min(abs(ret) * 0.001 * self.config.mip_gap, 0.01) + return ret # maybe do OBBT if self.current_node.tree_depth % self.config.node_obbt_frequency == 0 and self.current_node.tree_depth != 0: @@ -300,7 +305,12 @@ def bound(self): res.solution_loader.load_vars() self.relaxation_solution = res.solution_loader.get_primals() - return res.best_objective_bound + ret = res.best_objective_bound + if self.sense() == pybnb.minimize: + ret -= min(abs(ret) * 0.001 * self.config.mip_gap, 0.01) + else: + ret += min(abs(ret) * 0.001 * self.config.mip_gap, 0.01) + return ret def objective(self): if self.current_node.state.sol is not None: @@ -327,16 +337,20 @@ def objective(self): self.nlp.solutions.load_from(res) ret = pe.value(self.obj.expr) if self.sense == pybnb.minimize: - if ret < self.feasible_objective: + if self.feasible_objective is None or ret < self.feasible_objective: self.feasible_objective = ret else: - if ret > self.feasible_objective: + if self.feasible_objective is None or ret > self.feasible_objective: self.feasible_objective = ret sol = np.array([v.value for v in self.all_vars], dtype=float) self.current_node.state.sol = sol self.current_node.state.obj = ret for v in unfixed_vars: v.unfix() + if self.sense() == pybnb.minimize: + ret += min(abs(ret) * 0.001 * self.config.mip_gap, 0.01) + else: + ret -= min(abs(ret) * 0.001 * self.config.mip_gap, 0.01) return ret def get_state(self) -> NodeState: @@ -437,7 +451,7 @@ def branch(self): # the relaxation was feasible # no nodes in this part of the tree need explored return [] - + xl1 = xl.copy() xu1 = xu.copy() xl2 = xl.copy() @@ -468,7 +482,7 @@ def branch(self): def solve_with_bnb(model: _BlockData, config: BnBConfig): # we don't want to modify the original model model, orig_var_map = get_clone_and_var_map(model) - diving_obj, diving_sol = run_diving_heuristic(model, config.feasibility_tol, config.integer_tol) + diving_obj, diving_sol = run_diving_heuristic(model, config.feasibility_tol, config.integer_tol, node_limit=100) prob = _BnB(model, config, feasible_objective=diving_obj) res: pybnb.SolverResults = pybnb.solve( prob, @@ -486,12 +500,21 @@ def solve_with_bnb(model: _BlockData, config: BnBConfig): ret.best_feasible_objective = res.objective ret.best_objective_bound = res.bound ss = pybnb.SolutionStatus + tc = pybnb.TerminationCondition if res.solution_status == ss.optimal: ret.termination_condition = TerminationCondition.optimal elif res.solution_status == ss.infeasible: ret.termination_condition = TerminationCondition.infeasible elif res.solution_status == ss.unbounded: ret.termination_condition = TerminationCondition.unbounded + elif res.termination_condition == tc.time_limit: + ret.termination_condition = TerminationCondition.maxTimeLimit + elif res.termination_condition == tc.objective_limit: + ret.termination_condition = TerminationCondition.objectiveLimit + elif res.termination_condition == tc.node_limit: + ret.termination_condition = TerminationCondition.maxIterations + elif res.termination_condition == tc.interrupted: + ret.termination_condition = TerminationCondition.interrupted else: ret.termination_condition = TerminationCondition.unknown best_node = res.best_node diff --git a/pyomo/contrib/coramin/algorithms/bnb/tests/__init__.py b/pyomo/contrib/coramin/algorithms/bnb/tests/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/pyomo/contrib/coramin/algorithms/bnb/tests/test_bnb.py b/pyomo/contrib/coramin/algorithms/bnb/tests/test_bnb.py new file mode 100644 index 00000000000..e1eccb20387 --- /dev/null +++ b/pyomo/contrib/coramin/algorithms/bnb/tests/test_bnb.py @@ -0,0 +1,317 @@ +import math + +from pyomo.contrib import coramin +from pyomo.contrib.coramin.third_party.minlplib_tools import ( + get_minlplib, +) +from pyomo.common import unittest +from pyomo.contrib import appsi +import os +import logging +import math +from pyomo.common import download +import pyomo.environ as pe +from pyomo.core.base.block import _BlockData +import importlib +import shutil + + +def _get_sol(pname): + start_x1_set = {'batch0812', 'chem'} + current_dir = os.getcwd() + target_fname = os.path.join(current_dir, f'{pname}.sol') + downloader = download.FileDownloader() + downloader.set_destination_filename(target_fname) + downloader.get_binary_file(f'http://www.minlplib.org/sol/{pname}.p1.sol') + res = dict() + f = open(target_fname, 'r') + for line in f.readlines(): + l = line.split() + vname = l[0] + vval = float(l[1]) + if vname == 'objvar': + continue + assert vname.startswith('x') or vname.startswith('b') + if vname.startswith('x'): + ndx = int(vname.replace('x', '')) - 1 + if pname in start_x1_set: + ndx += 1 + vname = f'x{ndx}' + else: + ndx = int(vname.replace('b', '')) - 1 + if pname in start_x1_set: + ndx += 1 + vname = f'b{ndx}' + res[vname] = vval + f.close() + return res + + +class Helper(unittest.TestCase): + def _check_relative_diff(self, expected, got, abs_tol=1e-3, rel_tol=1e-3): + abs_diff = abs(expected - got) + if expected == 0: + rel_diff = math.inf + else: + rel_diff = abs_diff / abs(expected) + success = abs_diff <= abs_tol or rel_diff <= rel_tol + self.assertTrue( + success, + msg=f'\n expected: {expected}\n got: {got}\n abs diff: {abs_diff}\n rel diff: {rel_diff}', + ) + + +class TestBnBWithMINLPLib(Helper): + @classmethod + def setUpClass(self) -> None: + self.test_problems = { + 'batch0812': 2687026.784, + 'ball_mk3_10': None, + 'ball_mk2_10': 0, + 'syn05m': 837.73240090, + 'autocorr_bern20-03': -72, + 'chem': -47.70651483, + 'alkyl': -1.76499965, + } + self.primal_sol = dict() + self.primal_sol['batch0812'] = _get_sol('batch0812') + self.primal_sol['alkyl'] = _get_sol('alkyl') + self.primal_sol['ball_mk2_10'] = _get_sol('ball_mk2_10') + self.primal_sol['syn05m'] = _get_sol('syn05m') + self.primal_sol['autocorr_bern20-03'] = _get_sol('autocorr_bern20-03') + self.primal_sol['chem'] = _get_sol('chem') + for pname in self.test_problems.keys(): + get_minlplib(problem_name=pname, format='py') + self.opt = coramin.algorithms.BnBSolver() + self.opt.config.lp_solver = appsi.solvers.Highs() + self.opt.config.nlp_solver = pe.SolverFactory('ipopt') + + @classmethod + def tearDownClass(self) -> None: + current_dir = os.getcwd() + for pname in self.test_problems.keys(): + os.remove(os.path.join(current_dir, 'minlplib', 'py', f'{pname}.py')) + shutil.rmtree(os.path.join(current_dir, 'minlplib', 'py')) + os.rmdir(os.path.join(current_dir, 'minlplib')) + for pname in self.primal_sol.keys(): + os.remove(os.path.join(current_dir, f'{pname}.sol')) + + def get_model(self, pname): + current_dir = os.getcwd() + fname = os.path.join('minlplib', 'py', f'{pname}') + fname = fname.replace('/', '.') + m = importlib.import_module(fname).m + return m + + def _check_primal_sol(self, pname, m: _BlockData, res: appsi.base.Results): + expected_by_str = self.primal_sol[pname] + expected_by_var = pe.ComponentMap() + for vname, vval in expected_by_str.items(): + v = getattr(m, vname) + expected_by_var[v] = vval + got = res.solution_loader.get_primals() + for v, val in expected_by_var.items(): + self._check_relative_diff(val, got[v]) + got = res.solution_loader.get_primals(vars_to_load=list(expected_by_var.keys())) + for v, val in expected_by_var.items(): + self._check_relative_diff(val, got[v]) + + def optimal_helper(self, pname, check_primal_sol=True): + m = self.get_model(pname) + res = self.opt.solve(m) + self.assertEqual( + res.termination_condition, appsi.base.TerminationCondition.optimal + ) + self._check_relative_diff( + self.test_problems[pname], + res.best_feasible_objective, + abs_tol=self.opt.config.abs_gap, + rel_tol=self.opt.config.mip_gap, + ) + self._check_relative_diff( + self.test_problems[pname], + res.best_objective_bound, + abs_tol=self.opt.config.abs_gap, + rel_tol=self.opt.config.mip_gap, + ) + if check_primal_sol: + self._check_primal_sol(pname, m, res) + + def infeasible_helper(self, pname): + m = self.get_model(pname) + self.opt.config.load_solution = False + res = self.opt.solve(m) + self.assertEqual( + res.termination_condition, appsi.base.TerminationCondition.infeasible + ) + self.opt.config.load_solution = True + + def time_limit_helper(self, pname): + orig_time_limit = self.opt.config.time_limit + self.opt.config.load_solution = False + for new_limit in [0, 0.2]: + self.opt.config.time_limit = new_limit + m = self.get_model(pname) + res = self.opt.solve(m) + self.assertEqual( + res.termination_condition, appsi.base.TerminationCondition.maxTimeLimit + ) + self.opt.config.load_solution = True + self.opt.config.time_limit = orig_time_limit + + def test_batch0812(self): + self.optimal_helper('batch0812') + + def test_ball_mk2_10(self): + self.optimal_helper('ball_mk2_10') + + def test_alkyl(self): + self.optimal_helper('alkyl') + + def test_syn05m(self): + self.optimal_helper('syn05m') + + def test_autocorr_bern20_03(self): + self.optimal_helper('autocorr_bern20-03', check_primal_sol=False) + + def test_chem(self): + self.optimal_helper('chem') + + def test_time_limit(self): + self.time_limit_helper('chem') + + def test_ball_mk3_10(self): + self.infeasible_helper('ball_mk3_10') + + def test_available(self): + avail = self.opt.available() + assert avail in appsi.base.Solver.Availability + + +class TestMultiTree(Helper): + def test_convex_overestimator(self): + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(-2, 1)) + m.y = pe.Var() + m.obj = pe.Objective(expr=(m.x + 1) ** 2 - 0.2 * m.y) + m.c = pe.Constraint(expr=m.y <= m.x**2) + opt = coramin.algorithms.BnBSolver() + opt.config.lp_solver = appsi.solvers.Highs() + opt.config.nlp_solver = pe.SolverFactory('ipopt') + opt.config.mip_gap = 1e-6 + res = opt.solve(m) + self.assertEqual( + res.termination_condition, appsi.base.TerminationCondition.optimal + ) + self._check_relative_diff( + -0.25, + res.best_feasible_objective, + abs_tol=opt.config.abs_gap, + rel_tol=opt.config.mip_gap, + ) + self._check_relative_diff( + -0.25, + res.best_objective_bound, + abs_tol=opt.config.abs_gap, + rel_tol=opt.config.mip_gap, + ) + self._check_relative_diff(-1.250953, m.x.value, 1e-2, 1e-2) + self._check_relative_diff(1.5648825, m.y.value, 1e-2, 1e-2) + + def test_max_iter(self): + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(-2, 1)) + m.y = pe.Var() + m.obj = pe.Objective(expr=(m.x + 1) ** 2 - 0.2 * m.y) + m.c = pe.Constraint(expr=m.y <= m.x**2) + opt = coramin.algorithms.BnBSolver() + opt.config.lp_solver = appsi.solvers.Highs() + opt.config.nlp_solver = pe.SolverFactory('ipopt') + opt.config.node_limit = 1 + opt.config.load_solution = False + res = opt.solve(m) + self.assertEqual( + res.termination_condition, appsi.base.TerminationCondition.maxIterations + ) + + def test_nlp_infeas_fbbt(self): + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(-2, 1), domain=pe.Integers) + m.y = pe.Var(domain=pe.Integers, bounds=(-1000, 1000)) + m.obj = pe.Objective(expr=(m.x + 1) ** 2 - 0.2 * m.y) + m.c1 = pe.Constraint(expr=m.y <= (m.x - 0.5) ** 2 - 0.5) + m.c2 = pe.Constraint(expr=m.y >= -((m.x + 2) ** 2) + 4) + m.c3 = pe.Constraint(expr=m.y <= 2 * m.x + 7) + m.c4 = pe.Constraint(expr=m.y >= m.x) + opt = coramin.algorithms.BnBSolver() + opt.config.lp_solver = appsi.solvers.Highs() + opt.config.nlp_solver = pe.SolverFactory('ipopt') + opt.config.load_solution = False + res = opt.solve(m) + self.assertEqual( + res.termination_condition, appsi.base.TerminationCondition.infeasible + ) + + def test_all_vars_fixed_in_nlp(self): + m = pe.ConcreteModel() + m.x = pe.Var(bounds=(-2, 1)) + m.y = pe.Var(domain=pe.Integers, bounds=(-10000, 10000)) + m.z = pe.Var() + m.obj = pe.Objective(expr=m.z - 0.2 * m.y) + m.c1 = pe.Constraint(expr=m.y == (m.x - 0.5) ** 2 - 0.5) + m.c2 = pe.Constraint(expr=m.z == (m.x + 1) ** 2) + opt = coramin.algorithms.BnBSolver() + opt.config.lp_solver = appsi.solvers.Highs() + opt.config.nlp_solver = pe.SolverFactory('ipopt') + res = opt.solve(m) + self.assertEqual( + res.termination_condition, appsi.base.TerminationCondition.optimal + ) + self._check_relative_diff(-0.462486082, res.best_feasible_objective) + self._check_relative_diff(-0.462486082, res.best_objective_bound) + self._check_relative_diff(-1.37082869, m.x.value) + self._check_relative_diff(3, m.y.value) + self._check_relative_diff(0.137513918, m.z.value) + + def test_linear_problem(self): + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.obj = pe.Objective(expr=m.y) + m.c1 = pe.Constraint(expr=m.y >= m.x + 1) + m.c2 = pe.Constraint(expr=m.y >= -m.x + 1) + opt = coramin.algorithms.BnBSolver() + opt.config.lp_solver = appsi.solvers.Highs() + opt.config.nlp_solver = pe.SolverFactory('ipopt') + res = opt.solve(m) + self.assertEqual( + res.termination_condition, appsi.base.TerminationCondition.optimal + ) + self.assertAlmostEqual(res.best_feasible_objective, 1) + self.assertAlmostEqual(res.best_objective_bound, 1, 5) + self.assertAlmostEqual(m.x.value, 0) + self.assertAlmostEqual(m.y.value, 1) + + def test_stale_fixed_vars(self): + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.z = pe.Var(domain=pe.Binary) + m.w = pe.Var() + m.obj = pe.Objective(expr=m.y) + m.c1 = pe.Constraint(expr=m.y >= m.x + 1) + m.c2 = pe.Constraint(expr=m.y >= -m.x + 1) + m.c3 = pe.Constraint(expr=m.w == 2) + opt = coramin.algorithms.BnBSolver() + opt.config.lp_solver = appsi.solvers.Highs() + opt.config.nlp_solver = pe.SolverFactory('ipopt') + res = opt.solve(m) + self.assertEqual( + res.termination_condition, appsi.base.TerminationCondition.optimal + ) + self.assertAlmostEqual(res.best_feasible_objective, 1) + self.assertAlmostEqual(res.best_objective_bound, 1, 5) + self.assertAlmostEqual(m.x.value, 0) + self.assertAlmostEqual(m.y.value, 1) + self.assertAlmostEqual(m.w.value, 2) + self.assertIsNone(m.z.value) From 92163f2989dc99d4e14f63c658c0cc77e3d9dc7a Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 21 Feb 2024 09:15:41 -0700 Subject: [PATCH 095/128] cleanup --- .github/workflows/test_branches.yml | 2 +- .github/workflows/test_pr_and_main.yml | 2 +- pyomo/contrib/simplification/__init__.py | 11 +++++++++++ pyomo/contrib/simplification/build.py | 11 +++++++++++ 4 files changed, 24 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 83e652cbef8..57c24d99090 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -157,7 +157,7 @@ jobs: # path: cache/os # key: pkg-${{env.CACHE_VER}}.0-${{runner.os}} - - name: install ginac + - name: install GiNaC if: matrix.other == '/singletest' run: | cd .. diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index 6df28fbadc9..a55d100a18f 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -179,7 +179,7 @@ jobs: # path: cache/os # key: pkg-${{env.CACHE_VER}}.0-${{runner.os}} - - name: install ginac + - name: install GiNaC if: matrix.other == '/singletest' run: | cd .. diff --git a/pyomo/contrib/simplification/__init__.py b/pyomo/contrib/simplification/__init__.py index 3abe5a25ba0..b4fa68eb386 100644 --- a/pyomo/contrib/simplification/__init__.py +++ b/pyomo/contrib/simplification/__init__.py @@ -1 +1,12 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# 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. +# ___________________________________________________________________________ + from .simplify import Simplifier diff --git a/pyomo/contrib/simplification/build.py b/pyomo/contrib/simplification/build.py index 39742e1e351..67ae1d37335 100644 --- a/pyomo/contrib/simplification/build.py +++ b/pyomo/contrib/simplification/build.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# 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. +# ___________________________________________________________________________ + from pybind11.setup_helpers import Pybind11Extension, build_ext from pyomo.common.fileutils import this_file_dir, find_library import os From 0dff80fb37b9052805d744f42236711067d69bcc Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 21 Feb 2024 09:16:59 -0700 Subject: [PATCH 096/128] cleanup --- pyomo/contrib/simplification/build.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/simplification/build.py b/pyomo/contrib/simplification/build.py index 67ae1d37335..4bf28a0fa33 100644 --- a/pyomo/contrib/simplification/build.py +++ b/pyomo/contrib/simplification/build.py @@ -9,15 +9,16 @@ # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ -from pybind11.setup_helpers import Pybind11Extension, build_ext -from pyomo.common.fileutils import this_file_dir, find_library +import glob import os -from distutils.dist import Distribution -import sys import shutil -import glob +import sys import tempfile +from distutils.dist import Distribution + +from pybind11.setup_helpers import Pybind11Extension, build_ext from pyomo.common.envvar import PYOMO_CONFIG_DIR +from pyomo.common.fileutils import find_library, this_file_dir def build_ginac_interface(args=[]): From c49c2df810775f1c64702a26e3cf75c36f717c99 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 21 Feb 2024 09:23:10 -0700 Subject: [PATCH 097/128] cleanup --- pyomo/contrib/simplification/build.py | 11 ++++++----- pyomo/contrib/simplification/ginac_interface.cpp | 11 +++++++++++ pyomo/contrib/simplification/simplify.py | 11 +++++++++++ pyomo/contrib/simplification/tests/__init__.py | 11 +++++++++++ .../simplification/tests/test_simplification.py | 11 +++++++++++ 5 files changed, 50 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/simplification/build.py b/pyomo/contrib/simplification/build.py index 4bf28a0fa33..d9d1e701290 100644 --- a/pyomo/contrib/simplification/build.py +++ b/pyomo/contrib/simplification/build.py @@ -21,7 +21,9 @@ from pyomo.common.fileutils import find_library, this_file_dir -def build_ginac_interface(args=[]): +def build_ginac_interface(args=None): + if args is None: + args = list() dname = this_file_dir() _sources = ['ginac_interface.cpp'] sources = list() @@ -29,7 +31,6 @@ def build_ginac_interface(args=[]): sources.append(os.path.join(dname, fname)) ginac_lib = find_library('ginac') - print(ginac_lib) if ginac_lib is None: raise RuntimeError( 'could not find GiNaC library; please make sure it is in the LD_LIBRARY_PATH environment variable' @@ -62,7 +63,7 @@ def build_ginac_interface(args=[]): extra_compile_args=extra_args, ) - class ginac_build_ext(build_ext): + class ginacBuildExt(build_ext): def run(self): basedir = os.path.abspath(os.path.curdir) if self.inplace: @@ -72,7 +73,7 @@ def run(self): print("Building in '%s'" % tmpdir) os.chdir(tmpdir) try: - super(ginac_build_ext, self).run() + super(ginacBuildExt, self).run() if not self.inplace: library = glob.glob("build/*/ginac_interface.*")[0] target = os.path.join( @@ -94,7 +95,7 @@ def run(self): 'name': 'ginac_interface', 'packages': [], 'ext_modules': [ext], - 'cmdclass': {"build_ext": ginac_build_ext}, + 'cmdclass': {"build_ext": ginacBuildExt}, } dist = Distribution(package_config) diff --git a/pyomo/contrib/simplification/ginac_interface.cpp b/pyomo/contrib/simplification/ginac_interface.cpp index 32bea8dadd0..489f281bc2c 100644 --- a/pyomo/contrib/simplification/ginac_interface.cpp +++ b/pyomo/contrib/simplification/ginac_interface.cpp @@ -1,3 +1,14 @@ +// ___________________________________________________________________________ +// +// Pyomo: Python Optimization Modeling Objects +// Copyright (c) 2008-2022 +// 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. +// ___________________________________________________________________________ + #include "ginac_interface.hpp" diff --git a/pyomo/contrib/simplification/simplify.py b/pyomo/contrib/simplification/simplify.py index 4002f1a233f..b8cc4995f91 100644 --- a/pyomo/contrib/simplification/simplify.py +++ b/pyomo/contrib/simplification/simplify.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# 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. +# ___________________________________________________________________________ + from pyomo.core.expr.sympy_tools import sympy2pyomo_expression, sympyify_expression from pyomo.core.expr.numeric_expr import NumericExpression from pyomo.core.expr.numvalue import is_fixed, value diff --git a/pyomo/contrib/simplification/tests/__init__.py b/pyomo/contrib/simplification/tests/__init__.py index e69de29bb2d..9320e403e95 100644 --- a/pyomo/contrib/simplification/tests/__init__.py +++ b/pyomo/contrib/simplification/tests/__init__.py @@ -0,0 +1,11 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# 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. +# ___________________________________________________________________________ + diff --git a/pyomo/contrib/simplification/tests/test_simplification.py b/pyomo/contrib/simplification/tests/test_simplification.py index e3c60cb02ca..95402f98318 100644 --- a/pyomo/contrib/simplification/tests/test_simplification.py +++ b/pyomo/contrib/simplification/tests/test_simplification.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2022 +# 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. +# ___________________________________________________________________________ + from pyomo.common.unittest import TestCase from pyomo.common import unittest from pyomo.contrib.simplification import Simplifier From 29d6a19d0f1704a294d62d5a89370d6110a4fbe7 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Wed, 21 Feb 2024 09:29:35 -0700 Subject: [PATCH 098/128] cleanup --- pyomo/contrib/simplification/tests/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyomo/contrib/simplification/tests/__init__.py b/pyomo/contrib/simplification/tests/__init__.py index 9320e403e95..d93cfd77b3c 100644 --- a/pyomo/contrib/simplification/tests/__init__.py +++ b/pyomo/contrib/simplification/tests/__init__.py @@ -8,4 +8,3 @@ # rights in this software. # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ - From f1956505601c483b7fbcf191d34b57fe03b5c57e Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Thu, 22 Feb 2024 15:06:52 -0700 Subject: [PATCH 099/128] fix division by zero error in linear presolve --- .../solver/tests/solvers/test_solvers.py | 25 +++++++++++ pyomo/repn/plugins/nl_writer.py | 42 ++++++++++++------- 2 files changed, 51 insertions(+), 16 deletions(-) diff --git a/pyomo/contrib/solver/tests/solvers/test_solvers.py b/pyomo/contrib/solver/tests/solvers/test_solvers.py index cf5f6cf5c57..5962eee7a6b 100644 --- a/pyomo/contrib/solver/tests/solvers/test_solvers.py +++ b/pyomo/contrib/solver/tests/solvers/test_solvers.py @@ -1508,6 +1508,31 @@ def test_bug_2(self, name: str, opt_class: Type[SolverBase], use_presolve: bool) res = opt.solve(m) self.assertAlmostEqual(res.incumbent_objective, -18, 5) + @parameterized.expand(input=_load_tests(nl_solvers)) + def test_presolve_with_zero_coef(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): + opt: SolverBase = opt_class() + if not opt.available(): + raise unittest.SkipTest(f'Solver {opt.name} not available.') + + """ + when c2 gets presolved out, c1 becomes + x - y + y = 0 which becomes + x - 0*y == 0 which is the zero we are testing for + """ + m = pe.ConcreteModel() + m.x = pe.Var() + m.y = pe.Var() + m.z = pe.Var() + m.obj = pe.Objective(expr=m.x**2 + m.y**2 + m.z**2) + m.c1 = pe.Constraint(expr=m.x == m.y + m.z + 1.5) + m.c2 = pe.Constraint(expr=m.z == -m.y) + + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 2.25) + self.assertAlmostEqual(m.x.value, 1.5) + self.assertAlmostEqual(m.y.value, 0) + self.assertAlmostEqual(m.z.value, 0) + @parameterized.expand(input=_load_tests(all_solvers)) def test_scaling(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): opt: SolverBase = opt_class() diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index f3ff94ea8c9..18093739f40 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -1768,8 +1768,14 @@ def _linear_presolve(self, comp_by_linear_var, lcon_by_linear_nnz, var_bounds): # In an attempt to improve numerical stability, we will # solve for (and substitute out) the variable with the # coefficient closer to +/-1) - log_coef = _log10(abs(coef)) - log_coef2 = _log10(abs(coef2)) + if coef == 0: + log_coef = -inf + else: + log_coef = _log10(abs(coef)) + if coef2 == 0: + log_coef2 = -inf + else: + log_coef2 = _log10(abs(coef2)) if abs(log_coef2) < abs(log_coef) or ( log_coef2 == -log_coef and log_coef2 < log_coef ): @@ -1790,21 +1796,25 @@ def _linear_presolve(self, comp_by_linear_var, lcon_by_linear_nnz, var_bounds): b, ) # Tighten variable bounds - x_lb, x_ub = var_bounds[x] lb, ub = var_bounds[_id] - if lb is not None: - lb = (lb - b) / a - if ub is not None: - ub = (ub - b) / a - if a < 0: - lb, ub = ub, lb - if x_lb is None or (lb is not None and lb > x_lb): - x_lb = lb - if x_ub is None or (ub is not None and ub < x_ub): - x_ub = ub - var_bounds[x] = x_lb, x_ub - if x_lb == x_ub and x_lb is not None: - fixed_vars.append(x) + if a == 0: + if (lb is not None and lb > b) or (ub is not None and ub < b): + raise InfeasibleConstraintException(f'Model is infeasible: {lb} <= {var_map[_id]} <= {ub} and {var_map[_id]} == {a}*{var_map[x]} + {b} cannot both be satisfied') + else: + x_lb, x_ub = var_bounds[x] + if lb is not None: + lb = (lb - b) / a + if ub is not None: + ub = (ub - b) / a + if a < 0: + lb, ub = ub, lb + if x_lb is None or (lb is not None and lb > x_lb): + x_lb = lb + if x_ub is None or (ub is not None and ub < x_ub): + x_ub = ub + var_bounds[x] = x_lb, x_ub + if x_lb == x_ub and x_lb is not None: + fixed_vars.append(x) eliminated_cons.add(con_id) else: return eliminated_cons, eliminated_vars From fd656e05e77d453c92c42715793cc5a54ec5a46a Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Thu, 22 Feb 2024 15:19:00 -0700 Subject: [PATCH 100/128] run black --- pyomo/contrib/solver/tests/solvers/test_solvers.py | 6 ++++-- pyomo/repn/plugins/nl_writer.py | 4 +++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/solver/tests/solvers/test_solvers.py b/pyomo/contrib/solver/tests/solvers/test_solvers.py index 5962eee7a6b..e6f5e10f3f4 100644 --- a/pyomo/contrib/solver/tests/solvers/test_solvers.py +++ b/pyomo/contrib/solver/tests/solvers/test_solvers.py @@ -1509,11 +1509,13 @@ def test_bug_2(self, name: str, opt_class: Type[SolverBase], use_presolve: bool) self.assertAlmostEqual(res.incumbent_objective, -18, 5) @parameterized.expand(input=_load_tests(nl_solvers)) - def test_presolve_with_zero_coef(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): + def test_presolve_with_zero_coef( + self, name: str, opt_class: Type[SolverBase], use_presolve: bool + ): opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') - + """ when c2 gets presolved out, c1 becomes x - y + y = 0 which becomes diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index 18093739f40..7046e9a1de2 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -1799,7 +1799,9 @@ def _linear_presolve(self, comp_by_linear_var, lcon_by_linear_nnz, var_bounds): lb, ub = var_bounds[_id] if a == 0: if (lb is not None and lb > b) or (ub is not None and ub < b): - raise InfeasibleConstraintException(f'Model is infeasible: {lb} <= {var_map[_id]} <= {ub} and {var_map[_id]} == {a}*{var_map[x]} + {b} cannot both be satisfied') + raise InfeasibleConstraintException( + f'Model is infeasible: {lb} <= {var_map[_id]} <= {ub} and {var_map[_id]} == {a}*{var_map[x]} + {b} cannot both be satisfied' + ) else: x_lb, x_ub = var_bounds[x] if lb is not None: From 2aae9c505acfeb4172b69eb5c10ce6c251311cc3 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Sat, 24 Feb 2024 21:50:38 -0700 Subject: [PATCH 101/128] merge zero_presolve --- pyomo/repn/plugins/nl_writer.py | 64 ++++++++++++++++----------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/pyomo/repn/plugins/nl_writer.py b/pyomo/repn/plugins/nl_writer.py index 7046e9a1de2..a256cd1b900 100644 --- a/pyomo/repn/plugins/nl_writer.py +++ b/pyomo/repn/plugins/nl_writer.py @@ -1760,7 +1760,7 @@ def _linear_presolve(self, comp_by_linear_var, lcon_by_linear_nnz, var_bounds): id2_isdiscrete = var_map[id2].domain.isdiscrete() if var_map[_id].domain.isdiscrete() ^ id2_isdiscrete: # if only one variable is discrete, then we need to - # substiitute out the other + # substitute out the other if id2_isdiscrete: _id, id2 = id2, _id coef, coef2 = coef2, coef @@ -1768,14 +1768,8 @@ def _linear_presolve(self, comp_by_linear_var, lcon_by_linear_nnz, var_bounds): # In an attempt to improve numerical stability, we will # solve for (and substitute out) the variable with the # coefficient closer to +/-1) - if coef == 0: - log_coef = -inf - else: - log_coef = _log10(abs(coef)) - if coef2 == 0: - log_coef2 = -inf - else: - log_coef2 = _log10(abs(coef2)) + log_coef = _log10(abs(coef)) + log_coef2 = _log10(abs(coef2)) if abs(log_coef2) < abs(log_coef) or ( log_coef2 == -log_coef and log_coef2 < log_coef ): @@ -1796,27 +1790,21 @@ def _linear_presolve(self, comp_by_linear_var, lcon_by_linear_nnz, var_bounds): b, ) # Tighten variable bounds + x_lb, x_ub = var_bounds[x] lb, ub = var_bounds[_id] - if a == 0: - if (lb is not None and lb > b) or (ub is not None and ub < b): - raise InfeasibleConstraintException( - f'Model is infeasible: {lb} <= {var_map[_id]} <= {ub} and {var_map[_id]} == {a}*{var_map[x]} + {b} cannot both be satisfied' - ) - else: - x_lb, x_ub = var_bounds[x] - if lb is not None: - lb = (lb - b) / a - if ub is not None: - ub = (ub - b) / a - if a < 0: - lb, ub = ub, lb - if x_lb is None or (lb is not None and lb > x_lb): - x_lb = lb - if x_ub is None or (ub is not None and ub < x_ub): - x_ub = ub - var_bounds[x] = x_lb, x_ub - if x_lb == x_ub and x_lb is not None: - fixed_vars.append(x) + if lb is not None: + lb = (lb - b) / a + if ub is not None: + ub = (ub - b) / a + if a < 0: + lb, ub = ub, lb + if x_lb is None or (lb is not None and lb > x_lb): + x_lb = lb + if x_ub is None or (ub is not None and ub < x_ub): + x_ub = ub + var_bounds[x] = x_lb, x_ub + if x_lb == x_ub and x_lb is not None: + fixed_vars.append(x) eliminated_cons.add(con_id) else: return eliminated_cons, eliminated_vars @@ -1832,10 +1820,15 @@ def _linear_presolve(self, comp_by_linear_var, lcon_by_linear_nnz, var_bounds): # appropriately (that expr_info is persisting in the # eliminated_vars dict - and we will use that to # update other linear expressions later.) + old_nnz = len(expr_info.linear) c = expr_info.linear.pop(_id, 0) + nnz = old_nnz - 1 expr_info.const += c * b if x in expr_info.linear: expr_info.linear[x] += c * a + if expr_info.linear[x] == 0: + nnz -= 1 + coef = expr_info.linear.pop(x) elif a: expr_info.linear[x] = c * a # replacing _id with x... NNZ is not changing, @@ -1843,10 +1836,17 @@ def _linear_presolve(self, comp_by_linear_var, lcon_by_linear_nnz, var_bounds): # this constraint comp_by_linear_var[x].append((con_id, expr_info)) continue - # NNZ has been reduced by 1 - nnz = len(expr_info.linear) - _old = lcon_by_linear_nnz[nnz + 1] + _old = lcon_by_linear_nnz[old_nnz] if con_id in _old: + if not nnz: + if abs(expr_info.const) > TOL: + # constraint is trivially infeasible + raise InfeasibleConstraintException( + "model contains a trivially infeasible constraint " + f"{expr_info.const} == {coef}*{var_map[x]}" + ) + # constraint is trivially feasible + eliminated_cons.add(con_id) lcon_by_linear_nnz[nnz][con_id] = _old.pop(con_id) # If variables were replaced by the variable that # we are currently eliminating, then we need to update From 8d9926e572a7c9d06e5aeac65ed272041bf71e8d Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Sat, 24 Feb 2024 21:51:51 -0700 Subject: [PATCH 102/128] merge zero_presolve --- .../solver/tests/solvers/test_solvers.py | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/pyomo/contrib/solver/tests/solvers/test_solvers.py b/pyomo/contrib/solver/tests/solvers/test_solvers.py index e6f5e10f3f4..a4f4a3bc389 100644 --- a/pyomo/contrib/solver/tests/solvers/test_solvers.py +++ b/pyomo/contrib/solver/tests/solvers/test_solvers.py @@ -1515,6 +1515,10 @@ def test_presolve_with_zero_coef( opt: SolverBase = opt_class() if not opt.available(): raise unittest.SkipTest(f'Solver {opt.name} not available.') + if use_presolve: + opt.config.writer_config.linear_presolve = True + else: + opt.config.writer_config.linear_presolve = False """ when c2 gets presolved out, c1 becomes @@ -1535,6 +1539,40 @@ def test_presolve_with_zero_coef( self.assertAlmostEqual(m.y.value, 0) self.assertAlmostEqual(m.z.value, 0) + m.x.setlb(2) + res = opt.solve( + m, load_solutions=False, raise_exception_on_nonoptimal_result=False + ) + if use_presolve: + exp = TerminationCondition.provenInfeasible + else: + exp = TerminationCondition.locallyInfeasible + self.assertEqual(res.termination_condition, exp) + + m = pe.ConcreteModel() + m.w = pe.Var() + m.x = pe.Var() + m.y = pe.Var() + m.z = pe.Var() + m.obj = pe.Objective(expr=m.x**2 + m.y**2 + m.z**2 + m.w**2) + m.c1 = pe.Constraint(expr=m.x + m.w == m.y + m.z) + m.c2 = pe.Constraint(expr=m.z == -m.y) + m.c3 = pe.Constraint(expr=m.x == -m.w) + + res = opt.solve(m) + self.assertAlmostEqual(res.incumbent_objective, 0) + self.assertAlmostEqual(m.w.value, 0) + self.assertAlmostEqual(m.x.value, 0) + self.assertAlmostEqual(m.y.value, 0) + self.assertAlmostEqual(m.z.value, 0) + + del m.c1 + m.c1 = pe.Constraint(expr=m.x + m.w == m.y + m.z + 1.5) + res = opt.solve( + m, load_solutions=False, raise_exception_on_nonoptimal_result=False + ) + self.assertEqual(res.termination_condition, exp) + @parameterized.expand(input=_load_tests(all_solvers)) def test_scaling(self, name: str, opt_class: Type[SolverBase], use_presolve: bool): opt: SolverBase = opt_class() From 23131cec5c410fe35a801ba79952e9bb3f934d25 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Sat, 24 Feb 2024 22:09:14 -0700 Subject: [PATCH 103/128] run black --- pyomo/contrib/coramin/algorithms/alg_utils.py | 22 ++- pyomo/contrib/coramin/algorithms/bnb/bnb.py | 146 ++++++++++++----- .../coramin/algorithms/bnb/tests/test_bnb.py | 4 +- pyomo/contrib/coramin/algorithms/cut_gen.py | 38 ++++- .../coramin/algorithms/multitree/multitree.py | 14 +- .../multitree/tests/test_multitree.py | 4 +- .../coramin/cutting_planes/alpha_bb_cuts.py | 14 +- pyomo/contrib/coramin/domain_reduction/dbt.py | 32 ++-- .../contrib/coramin/domain_reduction/obbt.py | 8 +- .../domain_reduction/tests/test_dbt.py | 84 +++++++--- .../domain_reduction/tests/test_filters.py | 7 +- pyomo/contrib/coramin/examples/dbt.py | 9 +- pyomo/contrib/coramin/examples/dbt2.py | 9 +- .../binary_multiplication_reformulation.py | 114 +++++++------ pyomo/contrib/coramin/heuristics/diving.py | 106 +++++++++--- .../coramin/heuristics/feasibility_pump.py | 50 ++++-- .../heuristics/tests/test_heuristics.py | 2 +- pyomo/contrib/coramin/relaxations/alphabb.py | 4 +- .../contrib/coramin/relaxations/auto_relax.py | 153 +++++++++--------- .../contrib/coramin/relaxations/mccormick.py | 8 +- .../coramin/relaxations/multivariate.py | 4 +- .../coramin/relaxations/relaxations_base.py | 14 +- .../relaxations/tests/test_auto_relax.py | 14 +- .../relaxations/tests/test_relaxations.py | 8 +- .../contrib/coramin/relaxations/univariate.py | 48 +++--- pyomo/contrib/coramin/utils/compare_models.py | 46 ++++-- .../utils/tests/test_compare_models.py | 20 ++- 27 files changed, 623 insertions(+), 359 deletions(-) diff --git a/pyomo/contrib/coramin/algorithms/alg_utils.py b/pyomo/contrib/coramin/algorithms/alg_utils.py index 3647fdfa934..d856d690452 100644 --- a/pyomo/contrib/coramin/algorithms/alg_utils.py +++ b/pyomo/contrib/coramin/algorithms/alg_utils.py @@ -19,7 +19,9 @@ def collect_vars(m: _BlockData) -> Tuple[List[_GeneralVarData], List[_GeneralVar return list(binary_vars), list(integer_vars) -def relax_integers(binary_vars: Sequence[_GeneralVarData], integer_vars: Sequence[_GeneralVarData]): +def relax_integers( + binary_vars: Sequence[_GeneralVarData], integer_vars: Sequence[_GeneralVarData] +): for v in list(binary_vars) + list(integer_vars): lb, ub = v.bounds v.domain = pe.Reals @@ -31,7 +33,9 @@ def impose_structure(m): m.aux_vars = pe.VarList() for key, c in list(m.nonlinear.cons.items()): - repn: StandardRepn = generate_standard_repn(c.body, quadratic=False, compute_values=True) + repn: StandardRepn = generate_standard_repn( + c.body, quadratic=False, compute_values=True + ) expr_list = split_expr(repn.nonlinear_expr) if len(expr_list) == 1: continue @@ -51,13 +55,17 @@ def impose_structure(m): m.nonlinear.cons.add(v >= term) else: m.nonlinear.cons.add(v == term) - new_expr = LinearExpression(constant=repn.constant, linear_coefs=linear_coefs, linear_vars=linear_vars) + new_expr = LinearExpression( + constant=repn.constant, linear_coefs=linear_coefs, linear_vars=linear_vars + ) m.linear.cons.add((c.lb, new_expr, c.ub)) del m.nonlinear.cons[key] if hasattr(m.nonlinear, 'obj'): obj = m.nonlinear.obj - repn: StandardRepn = generate_standard_repn(obj.expr, quadratic=False, compute_values=True) + repn: StandardRepn = generate_standard_repn( + obj.expr, quadratic=False, compute_values=True + ) expr_list = split_expr(repn.nonlinear_expr) if len(expr_list) > 1: linear_coefs = list(repn.linear_coefs) @@ -72,6 +80,10 @@ def impose_structure(m): else: assert obj.sense == pe.maximize m.nonlinear.cons.add(v <= term) - new_expr = LinearExpression(constant=repn.constant, linear_coefs=linear_coefs, linear_vars=linear_vars) + new_expr = LinearExpression( + constant=repn.constant, + linear_coefs=linear_coefs, + linear_vars=linear_vars, + ) m.linear.obj = pe.Objective(expr=new_expr, sense=obj.sense) del m.nonlinear.obj diff --git a/pyomo/contrib/coramin/algorithms/bnb/bnb.py b/pyomo/contrib/coramin/algorithms/bnb/bnb.py index 33ca70676ec..aa2fb1f84f7 100644 --- a/pyomo/contrib/coramin/algorithms/bnb/bnb.py +++ b/pyomo/contrib/coramin/algorithms/bnb/bnb.py @@ -19,15 +19,19 @@ import numpy as np import logging from pyomo.contrib.appsi.base import ( - Solver, - MIPSolverConfig, - Results, - TerminationCondition, - SolutionLoader, + Solver, + MIPSolverConfig, + Results, + TerminationCondition, + SolutionLoader, SolverFactory, ) from pyomo.core.staleflag import StaleFlagManager -from pyomo.contrib.coramin.algorithms.alg_utils import impose_structure, collect_vars, relax_integers +from pyomo.contrib.coramin.algorithms.alg_utils import ( + impose_structure, + collect_vars, + relax_integers, +) from pyomo.contrib.coramin.algorithms.cut_gen import find_cut_generators, AlphaBBConfig @@ -46,19 +50,23 @@ def __init__(self): self.integer_tol = self.declare("integer_tol", ConfigValue(default=1e-4)) self.node_limit = self.declare("node_limit", ConfigValue(default=1000000000)) self.mip_gap = 1e-3 - self.num_root_obbt_iters = self.declare("num_root_obbt_iters", ConfigValue(default=3)) - self.node_obbt_frequency = self.declare("node_obbt_frequency", ConfigValue(default=2)) + self.num_root_obbt_iters = self.declare( + "num_root_obbt_iters", ConfigValue(default=3) + ) + self.node_obbt_frequency = self.declare( + "node_obbt_frequency", ConfigValue(default=2) + ) self.alphabb = self.declare("alphabb", AlphaBBConfig()) class NodeState(object): def __init__( - self, - lbs: np.ndarray, - ubs: np.ndarray, - parent: Optional[pybnb.Node], - sol: Optional[np.ndarray] = None, - obj: Optional[float] = None + self, + lbs: np.ndarray, + ubs: np.ndarray, + parent: Optional[pybnb.Node], + sol: Optional[np.ndarray] = None, + obj: Optional[float] = None, ) -> None: self.lbs: np.ndarray = lbs self.ubs: np.ndarray = ubs @@ -114,11 +122,15 @@ def __init__(self, model: _BlockData, config: BnBConfig, feasible_objective=None _fix_vars_with_close_bounds(relaxation.vars) impose_structure(relaxation) - self.cut_generators: List[CutGenerator] = find_cut_generators(relaxation, self.config.alphabb) + self.cut_generators: List[CutGenerator] = find_cut_generators( + relaxation, self.config.alphabb + ) _relax_cloned_model(relaxation) relaxation.cuts = pe.ConstraintList() self.relaxation_objects = list() - for r in iterators.relaxation_data_objects(relaxation, descend_into=True, active=True): + for r in iterators.relaxation_data_objects( + relaxation, descend_into=True, active=True + ): self.relaxation_objects.append(r) r.rebuild(build_nonlinear_constraint=True) self.interval_tightener.perform_fbbt(self.relaxation) @@ -139,9 +151,13 @@ def __init__(self, model: _BlockData, config: BnBConfig, feasible_objective=None other_vars = ComponentSet(i for i in relaxation.vars if i not in var_set) self.other_vars = other_vars = list(other_vars) - self.all_branching_vars = list(binary_vars) + list(integer_vars) + list(self.rhs_vars) + self.all_branching_vars = ( + list(binary_vars) + list(integer_vars) + list(self.rhs_vars) + ) self.all_vars = self.all_branching_vars + self.other_vars - self.var_to_ndx_map = ComponentMap((v, ndx) for ndx, v in enumerate(self.all_vars)) + self.var_to_ndx_map = ComponentMap( + (v, ndx) for ndx, v in enumerate(self.all_vars) + ) self.current_node: Optional[pybnb.Node] = None self.feasible_objective = feasible_objective @@ -201,22 +217,33 @@ def bound(self): if res.termination_condition == appsi.base.TerminationCondition.infeasible: return self.infeasible_objective() if res.termination_condition != appsi.base.TerminationCondition.optimal: - raise RuntimeError(f"Cannot handle termination condition {res.termination_condition} when solving relaxation") + raise RuntimeError( + f"Cannot handle termination condition {res.termination_condition} when solving relaxation" + ) res.solution_loader.load_vars() # add OA cuts for convex constraints while True: added_cuts = False for r in self.relaxation_objects: - new_con = r.add_cut(keep_cut=True, check_violation=True, feasibility_tol=self.config.feasibility_tol) + new_con = r.add_cut( + keep_cut=True, + check_violation=True, + feasibility_tol=self.config.feasibility_tol, + ) if new_con is not None: added_cuts = True if added_cuts: res = self.config.lp_solver.solve(self.relaxation) - if res.termination_condition == appsi.base.TerminationCondition.infeasible: + if ( + res.termination_condition + == appsi.base.TerminationCondition.infeasible + ): return self.infeasible_objective() if res.termination_condition != appsi.base.TerminationCondition.optimal: - raise RuntimeError(f"Cannot handle termination condition {res.termination_condition} when solving relaxation") + raise RuntimeError( + f"Cannot handle termination condition {res.termination_condition} when solving relaxation" + ) res.solution_loader.load_vars() else: break @@ -233,10 +260,15 @@ def bound(self): self.current_node.state.active_cut_indices.append(new_con_index) if added_cuts: res = self.config.lp_solver.solve(self.relaxation) - if res.termination_condition == appsi.base.TerminationCondition.infeasible: + if ( + res.termination_condition + == appsi.base.TerminationCondition.infeasible + ): return self.infeasible_objective() if res.termination_condition != appsi.base.TerminationCondition.optimal: - raise RuntimeError(f"Cannot handle termination condition {res.termination_condition} when solving relaxation") + raise RuntimeError( + f"Cannot handle termination condition {res.termination_condition} when solving relaxation" + ) res.solution_loader.load_vars() else: break @@ -269,7 +301,10 @@ def bound(self): return ret # maybe do OBBT - if self.current_node.tree_depth % self.config.node_obbt_frequency == 0 and self.current_node.tree_depth != 0: + if ( + self.current_node.tree_depth % self.config.node_obbt_frequency == 0 + and self.current_node.tree_depth != 0 + ): should_obbt = True if self._sense == pybnb.minimize: if self.feasible_objective is None: @@ -277,7 +312,10 @@ def bound(self): else: feasible_objective = self.feasible_objective feasible_objective += abs(feasible_objective) * 1e-3 + 1e-3 - if feasible_objective - res.best_objective_bound <= self.config.mip_gap * feasible_objective + self.config.abs_gap: + if ( + feasible_objective - res.best_objective_bound + <= self.config.mip_gap * feasible_objective + self.config.abs_gap + ): should_obbt = False else: if self.feasible_objective is None: @@ -285,7 +323,10 @@ def bound(self): else: feasible_objective = self.feasible_objective feasible_objective -= abs(feasible_objective) * 1e-3 + 1e-3 - if res.best_objective_bound - feasible_objective <= self.config.mip_gap * feasible_objective + self.config.abs_gap: + if ( + res.best_objective_bound - feasible_objective + <= self.config.mip_gap * feasible_objective + self.config.abs_gap + ): should_obbt = False if not math.isfinite(feasible_objective): feasible_objective = None @@ -300,7 +341,10 @@ def bound(self): for r in self.relaxation_objects: r.rebuild() res = self.config.lp_solver.solve(self.relaxation) - if res.termination_condition == appsi.base.TerminationCondition.infeasible: + if ( + res.termination_condition + == appsi.base.TerminationCondition.infeasible + ): return self.infeasible_objective() res.solution_loader.load_vars() self.relaxation_solution = res.solution_loader.get_primals() @@ -327,7 +371,9 @@ def objective(self): assert v.lb <= val <= v.ub v.fix(val) try: - res = self.config.nlp_solver.solve(self.nlp, load_solutions=False, skip_trivial_constraints=True, tee=False) + res = self.config.nlp_solver.solve( + self.nlp, load_solutions=False, skip_trivial_constraints=True, tee=False + ) success = True except: success = False @@ -451,7 +497,7 @@ def branch(self): # the relaxation was feasible # no nodes in this part of the tree need explored return [] - + xl1 = xl.copy() xu1 = xu.copy() xl2 = xl.copy() @@ -466,14 +512,18 @@ def branch(self): new_lb = math.ceil(new_lb) xu1[ndx_to_branch_on] = new_ub xl2[ndx_to_branch_on] = new_lb - + child1.state = NodeState(xl1, xu1, self.current_node, None, None) child2.state = NodeState(xl2, xu2, self.current_node, None, None) child1.state.valid_cut_indices = list(self.current_node.state.valid_cut_indices) child2.state.valid_cut_indices = list(self.current_node.state.valid_cut_indices) - child1.state.active_cut_indices = list(self.current_node.state.active_cut_indices) - child2.state.active_cut_indices = list(self.current_node.state.active_cut_indices) + child1.state.active_cut_indices = list( + self.current_node.state.active_cut_indices + ) + child2.state.active_cut_indices = list( + self.current_node.state.active_cut_indices + ) yield child1 yield child2 @@ -482,7 +532,9 @@ def branch(self): def solve_with_bnb(model: _BlockData, config: BnBConfig): # we don't want to modify the original model model, orig_var_map = get_clone_and_var_map(model) - diving_obj, diving_sol = run_diving_heuristic(model, config.feasibility_tol, config.integer_tol, node_limit=100) + diving_obj, diving_sol = run_diving_heuristic( + model, config.feasibility_tol, config.integer_tol, node_limit=100 + ) prob = _BnB(model, config, feasible_objective=diving_obj) res: pybnb.SolverResults = pybnb.solve( prob, @@ -520,7 +572,15 @@ def solve_with_bnb(model: _BlockData, config: BnBConfig): best_node = res.best_node if best_node is None: if diving_obj is not None: - ret.solution_loader = SolutionLoader(primals={id(orig_var_map[v]): (orig_var_map[v], val) for v, val in diving_sol.items()}, duals=None, slacks=None, reduced_costs=None) + ret.solution_loader = SolutionLoader( + primals={ + id(orig_var_map[v]): (orig_var_map[v], val) + for v, val in diving_sol.items() + }, + duals=None, + slacks=None, + reduced_costs=None, + ) else: vals = best_node.state.sol primals = dict() @@ -529,7 +589,9 @@ def solve_with_bnb(model: _BlockData, config: BnBConfig): if v in orig_vars: ov = orig_var_map[v] primals[id(ov)] = (ov, val) - ret.solution_loader = SolutionLoader(primals=primals, duals=None, slacks=None, reduced_costs=None) + ret.solution_loader = SolutionLoader( + primals=primals, duals=None, slacks=None, reduced_costs=None + ) return ret @@ -540,24 +602,26 @@ def __init__(self) -> None: def available(self): return self.Availability.FullLicense - + def version(self) -> Tuple: return (1, 0, 0) @property def config(self): return self._config - + @property def symbol_map(self): raise NotImplementedError('do this') - + def solve(self, model: _BlockData, timer: HierarchicalTimer = None) -> Results: StaleFlagManager.mark_all_as_stale() res = solve_with_bnb(model, self.config) if self.config.load_solution: res.solution_loader.load_vars() return res - -SolverFactory.register(name="coramin_bnb", doc="Coramin Branch and Bound Solver")(BnBSolver) + +SolverFactory.register(name="coramin_bnb", doc="Coramin Branch and Bound Solver")( + BnBSolver +) diff --git a/pyomo/contrib/coramin/algorithms/bnb/tests/test_bnb.py b/pyomo/contrib/coramin/algorithms/bnb/tests/test_bnb.py index e1eccb20387..f193ecb44a9 100644 --- a/pyomo/contrib/coramin/algorithms/bnb/tests/test_bnb.py +++ b/pyomo/contrib/coramin/algorithms/bnb/tests/test_bnb.py @@ -1,9 +1,7 @@ import math from pyomo.contrib import coramin -from pyomo.contrib.coramin.third_party.minlplib_tools import ( - get_minlplib, -) +from pyomo.contrib.coramin.third_party.minlplib_tools import get_minlplib from pyomo.common import unittest from pyomo.contrib import appsi import os diff --git a/pyomo/contrib/coramin/algorithms/cut_gen.py b/pyomo/contrib/coramin/algorithms/cut_gen.py index 78c58f57b54..c08760bf924 100644 --- a/pyomo/contrib/coramin/algorithms/cut_gen.py +++ b/pyomo/contrib/coramin/algorithms/cut_gen.py @@ -10,32 +10,58 @@ class AlphaBBConfig(ConfigDict): - def __init__(self, description=None, doc=None, implicit=False, implicit_domain=None, visibility=0): + def __init__( + self, + description=None, + doc=None, + implicit=False, + implicit_domain=None, + visibility=0, + ): super().__init__(description, doc, implicit, implicit_domain, visibility) self.max_num_vars: int = self.declare("max_num_vars", ConfigValue(default=4)) - self.method: EigenValueBounder = self.declare("method", ConfigValue(default=EigenValueBounder.GershgorinWithSimplification)) + self.method: EigenValueBounder = self.declare( + "method", + ConfigValue(default=EigenValueBounder.GershgorinWithSimplification), + ) def find_cut_generators(m: _BlockData, config: AlphaBBConfig) -> List[CutGenerator]: cut_generators = list() for c in m.nonlinear.cons.values(): - repn: StandardRepn = generate_standard_repn(c.body, quadratic=False, compute_values=True) + repn: StandardRepn = generate_standard_repn( + c.body, quadratic=False, compute_values=True + ) if repn.nonlinear_expr is None: continue if len(repn.nonlinear_vars) > config.max_num_vars: continue if len(repn.linear_coefs) > 0: - lhs = LinearExpression(constant=repn.constant, linear_coefs=repn.linear_coefs, linear_vars=repn.linear_vars) + lhs = LinearExpression( + constant=repn.constant, + linear_coefs=repn.linear_coefs, + linear_vars=repn.linear_vars, + ) else: lhs = repn.constant # alpha bb convention is lhs >= rhs if c.lb is not None: - cg = AlphaBBCutGenerator(lhs=lhs - c.lb, rhs=-repn.nonlinear_expr, eigenvalue_opt=None, method=config.method) + cg = AlphaBBCutGenerator( + lhs=lhs - c.lb, + rhs=-repn.nonlinear_expr, + eigenvalue_opt=None, + method=config.method, + ) cut_generators.append(cg) if c.ub is not None: - cg = AlphaBBCutGenerator(lhs=c.ub - lhs, rhs=repn.nonlinear_expr, eigenvalue_opt=None, method=config.method) + cg = AlphaBBCutGenerator( + lhs=c.ub - lhs, + rhs=repn.nonlinear_expr, + eigenvalue_opt=None, + method=config.method, + ) cut_generators.append(cg) return cut_generators diff --git a/pyomo/contrib/coramin/algorithms/multitree/multitree.py b/pyomo/contrib/coramin/algorithms/multitree/multitree.py index 6f1d4da6a36..e605d85d14b 100644 --- a/pyomo/contrib/coramin/algorithms/multitree/multitree.py +++ b/pyomo/contrib/coramin/algorithms/multitree/multitree.py @@ -46,7 +46,11 @@ from pyomo.core.expr.visitor import identify_variables from pyomo.contrib.coramin.clone import clone_shallow_active_flat, get_clone_and_var_map from pyomo.contrib.coramin.algorithms.cut_gen import AlphaBBConfig, find_cut_generators -from pyomo.contrib.coramin.algorithms.alg_utils import impose_structure, collect_vars, relax_integers +from pyomo.contrib.coramin.algorithms.alg_utils import ( + impose_structure, + collect_vars, + relax_integers, +) logger = logging.getLogger(__name__) @@ -569,9 +573,7 @@ def _solve_nlp_with_fixed_vars( nlp_res.best_feasible_objective = pe.value(nlp_obj) nlp_res.best_objective_bound = nlp_res.best_feasible_objective nlp_res.solution_loader = MultiTreeSolutionLoader( - pe.ComponentMap( - (v, v.value) for v in self._nlp.vars - ) + pe.ComponentMap((v, v.value) for v in self._nlp.vars) ) self._update_primal_bound(nlp_res) @@ -967,7 +969,9 @@ def solve( ) vars_to_tighten = ComponentSet() - for r in relaxation_data_objects(self._relaxation, descend_into=True, active=True): + for r in relaxation_data_objects( + self._relaxation, descend_into=True, active=True + ): vars_to_tighten.update(r.get_rhs_vars()) vars_to_tighten = list(vars_to_tighten) for obbt_iter in range(self.config.root_obbt_max_iter): diff --git a/pyomo/contrib/coramin/algorithms/multitree/tests/test_multitree.py b/pyomo/contrib/coramin/algorithms/multitree/tests/test_multitree.py index 6dcdf87c2c8..181957769c1 100644 --- a/pyomo/contrib/coramin/algorithms/multitree/tests/test_multitree.py +++ b/pyomo/contrib/coramin/algorithms/multitree/tests/test_multitree.py @@ -1,9 +1,7 @@ import math from pyomo.contrib import coramin -from pyomo.contrib.coramin.third_party.minlplib_tools import ( - get_minlplib, -) +from pyomo.contrib.coramin.third_party.minlplib_tools import get_minlplib from pyomo.common import unittest from pyomo.contrib import appsi import os diff --git a/pyomo/contrib/coramin/cutting_planes/alpha_bb_cuts.py b/pyomo/contrib/coramin/cutting_planes/alpha_bb_cuts.py index b522b352556..8c4f9a45e43 100644 --- a/pyomo/contrib/coramin/cutting_planes/alpha_bb_cuts.py +++ b/pyomo/contrib/coramin/cutting_planes/alpha_bb_cuts.py @@ -15,16 +15,18 @@ class AlphaBBCutGenerator(CutGenerator): def __init__( - self, - lhs: Union[float, int, NumericValue], - rhs: NumericExpression, - eigenvalue_opt: Optional[Solver] = None, + self, + lhs: Union[float, int, NumericValue], + rhs: NumericExpression, + eigenvalue_opt: Optional[Solver] = None, method: EigenValueBounder = EigenValueBounder.GershgorinWithSimplification, feasibility_tol: float = 1e-6, ) -> None: self.lhs = lhs self.rhs = rhs - self.xlist: List[_GeneralVarData] = list(identify_variables(rhs, include_fixed=False)) + self.xlist: List[_GeneralVarData] = list( + identify_variables(rhs, include_fixed=False) + ) self.hessian = Hessian(expr=rhs, opt=eigenvalue_opt, method=method) self.feasibility_tol = feasibility_tol self._proven_convex = dict() @@ -69,5 +71,5 @@ def generate(self, node: Optional[pybnb.Node]) -> Optional[RelationalExpression] alpha_bb_rhs = self.rhs + alpha * alpha_sum if lhs_val + self.feasibility_tol >= value(alpha_bb_rhs): return None - + return self.lhs >= taylor_series_expansion(alpha_bb_rhs) diff --git a/pyomo/contrib/coramin/domain_reduction/dbt.py b/pyomo/contrib/coramin/domain_reduction/dbt.py index 7abcd668aeb..d711866bdea 100644 --- a/pyomo/contrib/coramin/domain_reduction/dbt.py +++ b/pyomo/contrib/coramin/domain_reduction/dbt.py @@ -238,7 +238,10 @@ def build_pyomo_model(self, block): block: TreeBlockData empty TreeBlock """ - block.setup(children_keys=list(range(len(self.children))), coupling_vars=[i.comp for i in self.coupling_vars]) + block.setup( + children_keys=list(range(len(self.children))), + coupling_vars=[i.comp for i in self.coupling_vars], + ) for i, child in enumerate(self.children): if isinstance(child, _Tree): @@ -401,7 +404,10 @@ def _refine_partition( new_body = flatten_expr(c.body) - if type(new_body) not in {numeric_expr.SumExpression, numeric_expr.LinearExpression}: + if type(new_body) not in { + numeric_expr.SumExpression, + numeric_expr.LinearExpression, + }: logger.info( f'Constraint {str(c)} is contributing to {count} removed ' f'edges, but we cannot split the constraint because the ' @@ -555,7 +561,7 @@ def split_metis(graph, model): for n1, n2 in graph.edges(): assert n1.is_var() != n2.is_var() # xor if n2.is_var(): - n1, n2, = n2, n1 + n1, n2 = n2, n1 if n2 in graph_a_nodes: graph_a_edges.append((n1, n2)) else: @@ -838,7 +844,10 @@ def _decompose_model( logger.debug( 'partitioning ratio: {ratio}'.format(ratio=partitioning_ratio) ) - if min_partition_ratio <= 0 or partitioning_ratio > min_partition_ratio: + if ( + min_partition_ratio <= 0 + or partitioning_ratio > min_partition_ratio + ): logger.debug('partitioned {0}'.format(str(_graph))) _parent.children.discard(_graph) _parent.children.add(sub_tree) @@ -874,10 +883,7 @@ def _decompose_model( obj = get_objective(model) if obj is not None: - new_model.obj = pe.Objective( - expr=obj.expr, - sense=obj.sense, - ) + new_model.obj = pe.Objective(expr=obj.expr, sense=obj.sense) logger.debug('done adding objective to new model') else: logger.debug('No objective was found to add to the new model') @@ -932,15 +938,9 @@ def collect_vars_to_tighten_from_graph(graph): for n in graph.nodes(): if n.is_rel(): rel: BaseRelaxationData = n.comp - if ( - rel.is_rhs_convex() - and rel.relaxation_side == RelaxationSide.UNDER - ): + if rel.is_rhs_convex() and rel.relaxation_side == RelaxationSide.UNDER: continue - if ( - rel.is_rhs_concave() - and rel.relaxation_side == RelaxationSide.OVER - ): + if rel.is_rhs_concave() and rel.relaxation_side == RelaxationSide.OVER: continue vars_to_tighten.update(rel.get_rhs_vars()) elif n.is_var(): diff --git a/pyomo/contrib/coramin/domain_reduction/obbt.py b/pyomo/contrib/coramin/domain_reduction/obbt.py index 5272d2ccc1e..b00f2b3ecf7 100644 --- a/pyomo/contrib/coramin/domain_reduction/obbt.py +++ b/pyomo/contrib/coramin/domain_reduction/obbt.py @@ -456,11 +456,9 @@ def perform_obbt( obbt_info.num_successful_problems = 0 t0 = time.time() - ( - deactivated_objectives, - orig_update_config, - orig_config, - ) = _bt_prep(model=model, solver=solver, objective_bound=objective_bound) + (deactivated_objectives, orig_update_config, orig_config) = _bt_prep( + model=model, solver=solver, objective_bound=objective_bound + ) vardata_list = _build_vardatalist( model=model, varlist=varlist, warning_threshold=warning_threshold diff --git a/pyomo/contrib/coramin/domain_reduction/tests/test_dbt.py b/pyomo/contrib/coramin/domain_reduction/tests/test_dbt.py index a58375167e6..b771f732464 100644 --- a/pyomo/contrib/coramin/domain_reduction/tests/test_dbt.py +++ b/pyomo/contrib/coramin/domain_reduction/tests/test_dbt.py @@ -43,8 +43,8 @@ def test_decomp1(self): c.add(x[1] == x[2] + x[3]) c.add(x[4] == x[5] + x[6]) - c.add(x[2] <= 2*x[3] + 1) - c.add(x[5] >= 2*x[6] + 1) + c.add(x[2] <= 2 * x[3] + 1) + c.add(x[5] >= 2 * x[6] + 1) m2, reason = decompose_model(m1) self.assertEqual(reason, DecompositionStatus.normal) @@ -67,14 +67,14 @@ def test_decomp2(self): c.add(x[1] == x[2] + x[3]) c.add(x[4] == x[5] + x[6]) - c.add(x[2] <= 2*x[3] + 1) - c.add(x[5] >= 2*x[6] + 1) + c.add(x[2] <= 2 * x[3] + 1) + c.add(x[5] >= 2 * x[6] + 1) c.add(x[1] == x[4]) c.add(x[7] == x[8] + x[9]) c.add(x[10] == x[11] + x[12]) - c.add(x[8] <= 2*x[9] + 1) - c.add(x[11] >= 2*x[12] + 1) + c.add(x[8] <= 2 * x[9] + 1) + c.add(x[11] >= 2 * x[12] + 1) c.add(x[7] == x[10]) m2, reason = decompose_model(m1, limit_num_stages=False) @@ -90,7 +90,15 @@ def test_decomp2(self): self.assertEqual(m2.get_block_stage(m2.children[1]), 1) self.assertEqual(list(m2.stage_blocks(0)), [m2]) self.assertEqual(list(m2.stage_blocks(1)), [m2.children[0], m2.children[1]]) - self.assertEqual(list(m2.stage_blocks(2)), [m2.children[0].children[0], m2.children[0].children[1], m2.children[1].children[0], m2.children[1].children[1]]) + self.assertEqual( + list(m2.stage_blocks(2)), + [ + m2.children[0].children[0], + m2.children[0].children[1], + m2.children[1].children[0], + m2.children[1].children[1], + ], + ) for b in [m2.children[0], m2.children[1]]: self.assertEqual(len(b.children), 2) @@ -120,10 +128,46 @@ def test_decomp3(self): self.assertEqual(reason, DecompositionStatus.normal) self.assertTrue(is_equivalent(m1, m2, appsi.solvers.Highs())) self.assertEqual(len(m2.children), 2) - self.assertEqual(len(list(coramin.relaxations.iterators.nonrelaxation_component_data_objects(m2.children[0], pe.Constraint, descend_into=True, active=True))), 1) - self.assertEqual(len(list(coramin.relaxations.iterators.nonrelaxation_component_data_objects(m2.children[1], pe.Constraint, descend_into=True, active=True))), 1) - self.assertEqual(len(list(coramin.relaxations.iterators.relaxation_data_objects(m2.children[0], descend_into=True, active=True))), 1) - self.assertEqual(len(list(coramin.relaxations.iterators.relaxation_data_objects(m2.children[1], descend_into=True, active=True))), 1) + self.assertEqual( + len( + list( + coramin.relaxations.iterators.nonrelaxation_component_data_objects( + m2.children[0], pe.Constraint, descend_into=True, active=True + ) + ) + ), + 1, + ) + self.assertEqual( + len( + list( + coramin.relaxations.iterators.nonrelaxation_component_data_objects( + m2.children[1], pe.Constraint, descend_into=True, active=True + ) + ) + ), + 1, + ) + self.assertEqual( + len( + list( + coramin.relaxations.iterators.relaxation_data_objects( + m2.children[0], descend_into=True, active=True + ) + ) + ), + 1, + ) + self.assertEqual( + len( + list( + coramin.relaxations.iterators.relaxation_data_objects( + m2.children[1], descend_into=True, active=True + ) + ) + ), + 1, + ) self.assertEqual(len(list(active_vars(m2.children[0]))), 3) self.assertEqual(len(list(active_vars(m2.children[1]))), 3) self.assertEqual(m2.get_block_stage(m2), 0) @@ -141,8 +185,8 @@ def test_objective(self): c.add(x[1] == x[2] + x[3]) c.add(x[4] == x[5] + x[6]) - c.add(x[2] <= 2*x[3] + 1) - c.add(x[5] >= 2*x[6] + 1) + c.add(x[2] <= 2 * x[3] + 1) + c.add(x[5] >= 2 * x[6] + 1) m1.obj = pe.Objective(expr=sum(x.values())) m2, reason = decompose_model(m1) @@ -150,7 +194,9 @@ def test_objective(self): opt = appsi.solvers.Highs() res1 = opt.solve(m1) res2 = opt.solve(m2) - self.assertAlmostEqual(res1.best_feasible_objective, res2.best_feasible_objective) + self.assertAlmostEqual( + res1.best_feasible_objective, res2.best_feasible_objective + ) self.assertEqual(len(m2.children), 2) self.assertIn(len(list(active_cons(m2.children[0]))), {3, 4}) self.assertIn(len(list(active_cons(m2.children[1]))), {3, 4}) @@ -169,14 +215,14 @@ def test_refine_partition(self): c.add(x[1] == x[2] + x[3]) c.add(x[4] == x[5] + x[6]) - c.add(x[2] <= 2*x[3] + 1) - c.add(x[5] >= 2*x[6] + 1) + c.add(x[2] <= 2 * x[3] + 1) + c.add(x[5] >= 2 * x[6] + 1) c.add(x[1] == x[4]) c.add(x[7] == x[8] + x[9]) c.add(x[10] == x[11] + x[12]) - c.add(x[8] <= 2*x[9] + 1) - c.add(x[11] >= 2*x[12] + 1) + c.add(x[8] <= 2 * x[9] + 1) + c.add(x[11] >= 2 * x[12] + 1) c.add(x[7] == x[10]) c.add(sum(x.values()) == 1) @@ -715,7 +761,7 @@ def test_dbt_with_filter(self): self.assertAlmostEqual(b0.y.ub, 1) self.assertAlmostEqual(b1.x.lb, -1) self.assertAlmostEqual(b1.x.ub, 1) - + class TestDBTWithECP(unittest.TestCase): def create_model(self): diff --git a/pyomo/contrib/coramin/domain_reduction/tests/test_filters.py b/pyomo/contrib/coramin/domain_reduction/tests/test_filters.py index 69cadda292a..73a32461aff 100644 --- a/pyomo/contrib/coramin/domain_reduction/tests/test_filters.py +++ b/pyomo/contrib/coramin/domain_reduction/tests/test_filters.py @@ -14,10 +14,9 @@ def test_basic_filter(self): r = coramin.relaxations.relax(m) opt = appsi.solvers.Highs() res = opt.solve(r) - ( - vars_to_min, - vars_to_max, - ) = coramin.domain_reduction.filter_variables_from_solution([m.x]) + (vars_to_min, vars_to_max) = ( + coramin.domain_reduction.filter_variables_from_solution([m.x]) + ) self.assertIn(m.x, vars_to_max) self.assertNotIn(m.x, vars_to_min) diff --git a/pyomo/contrib/coramin/examples/dbt.py b/pyomo/contrib/coramin/examples/dbt.py index 3284ed534ef..bf3e9c0eadc 100644 --- a/pyomo/contrib/coramin/examples/dbt.py +++ b/pyomo/contrib/coramin/examples/dbt.py @@ -2,6 +2,7 @@ This example demonstrates how to used decomposed bounds tightening. The example problem is an ACOPF problem. """ + import pyomo.environ as pe from pyomo.contrib import coramin from egret.data.model_data import ModelData @@ -25,11 +26,9 @@ # perform decomposition print('Decomposing relaxation') -( - relaxation, - component_map, - termination_reason, -) = coramin.domain_reduction.decompose_model(relaxation, max_leaf_nnz=1000) +(relaxation, component_map, termination_reason) = ( + coramin.domain_reduction.decompose_model(relaxation, max_leaf_nnz=1000) +) # Add more outer approximation points for the second order cone constraints print('Adding extra outer-approximation points for SOC constraints') diff --git a/pyomo/contrib/coramin/examples/dbt2.py b/pyomo/contrib/coramin/examples/dbt2.py index a2520a96d36..57a62caab02 100644 --- a/pyomo/contrib/coramin/examples/dbt2.py +++ b/pyomo/contrib/coramin/examples/dbt2.py @@ -5,6 +5,7 @@ the filename in the "read_osil" function bedlow. The file can be downloaded from minlplib.org. Suspect is also needed. """ + import pyomo.environ as pe from pyomo.contrib import coramin import itertools @@ -31,11 +32,9 @@ # perform decomposition print('Decomposing relaxation') -( - relaxation, - component_map, - termination_reason, -) = coramin.domain_reduction.decompose_model(relaxation, max_leaf_nnz=1000) +(relaxation, component_map, termination_reason) = ( + coramin.domain_reduction.decompose_model(relaxation, max_leaf_nnz=1000) +) # rebuild the relaxations for b in coramin.relaxations.relaxation_data_objects( diff --git a/pyomo/contrib/coramin/heuristics/binary_multiplication_reformulation.py b/pyomo/contrib/coramin/heuristics/binary_multiplication_reformulation.py index 55231892c56..ae57e148429 100644 --- a/pyomo/contrib/coramin/heuristics/binary_multiplication_reformulation.py +++ b/pyomo/contrib/coramin/heuristics/binary_multiplication_reformulation.py @@ -24,8 +24,8 @@ def __init__(self, m: _BlockData) -> None: def handle_var( - node: Union[NumericValue, float], - args: Sequence[Union[NumericValue, float]], + node: Union[NumericValue, float], + args: Sequence[Union[NumericValue, float]], info: BinaryMultiplicationInfo, ): if node.is_fixed(): @@ -36,40 +36,40 @@ def handle_var( def handle_float( - node: Union[NumericValue, float], - args: Sequence[Union[NumericValue, float]], + node: Union[NumericValue, float], + args: Sequence[Union[NumericValue, float]], info: BinaryMultiplicationInfo, ): return node def handle_param( - node: Union[NumericValue, float], - args: Sequence[Union[NumericValue, float]], + node: Union[NumericValue, float], + args: Sequence[Union[NumericValue, float]], info: BinaryMultiplicationInfo, ): return node.value def handle_sum( - node: Union[NumericValue, float], - args: Sequence[Union[NumericValue, float]], + node: Union[NumericValue, float], + args: Sequence[Union[NumericValue, float]], info: BinaryMultiplicationInfo, ): return sum(args) def handle_monomial( - node: Union[NumericValue, float], - args: Sequence[Union[NumericValue, float]], + node: Union[NumericValue, float], + args: Sequence[Union[NumericValue, float]], info: BinaryMultiplicationInfo, ): - return args[0]*args[1] + return args[0] * args[1] def handle_product( - node: Union[NumericValue, float], - args: Sequence[Union[NumericValue, float]], + node: Union[NumericValue, float], + args: Sequence[Union[NumericValue, float]], info: BinaryMultiplicationInfo, ): x, y = args @@ -78,7 +78,9 @@ def handle_product( if xtype in native_numeric_types or ytype in native_numeric_types: return x * y - if ((x.is_variable_type() and x.is_binary()) or (y.is_variable_type() and y.is_binary())): + if (x.is_variable_type() and x.is_binary()) or ( + y.is_variable_type() and y.is_binary() + ): def get_new_rel(m): ndx = len(m.relaxations) @@ -101,14 +103,20 @@ def get_new_rel(m): clb, cub = info.constraint_bounds if clb == cub and clb is not None: rel = get_new_rel(info.m) - rel.build(_x, _y, clb, relaxation_side=RelaxationSide.BOTH, safety_tol=0) + rel.build( + _x, _y, clb, relaxation_side=RelaxationSide.BOTH, safety_tol=0 + ) else: if clb is not None: rel = get_new_rel(info.m) - rel.build(_x, _y, clb, relaxation_side=RelaxationSide.OVER, safety_tol=0) + rel.build( + _x, _y, clb, relaxation_side=RelaxationSide.OVER, safety_tol=0 + ) if cub is not None: rel = get_new_rel(info.m) - rel.build(_x, _y, cub, relaxation_side=RelaxationSide.UNDER, safety_tol=0) + rel.build( + _x, _y, cub, relaxation_side=RelaxationSide.UNDER, safety_tol=0 + ) return None else: z = info.m.vars.add() @@ -122,96 +130,96 @@ def get_new_rel(m): def handle_exp( - node: Union[NumericValue, float], - args: Sequence[Union[NumericValue, float]], + node: Union[NumericValue, float], + args: Sequence[Union[NumericValue, float]], info: BinaryMultiplicationInfo, ): return pe.exp(args[0]) def handle_log( - node: Union[NumericValue, float], - args: Sequence[Union[NumericValue, float]], + node: Union[NumericValue, float], + args: Sequence[Union[NumericValue, float]], info: BinaryMultiplicationInfo, ): return pe.log(args[0]) def handle_log10( - node: Union[NumericValue, float], - args: Sequence[Union[NumericValue, float]], + node: Union[NumericValue, float], + args: Sequence[Union[NumericValue, float]], info: BinaryMultiplicationInfo, ): return pe.log10(args[0]) def handle_sin( - node: Union[NumericValue, float], - args: Sequence[Union[NumericValue, float]], + node: Union[NumericValue, float], + args: Sequence[Union[NumericValue, float]], info: BinaryMultiplicationInfo, ): return pe.sin(args[0]) def handle_cos( - node: Union[NumericValue, float], - args: Sequence[Union[NumericValue, float]], + node: Union[NumericValue, float], + args: Sequence[Union[NumericValue, float]], info: BinaryMultiplicationInfo, ): return pe.cos(args[0]) def handle_tan( - node: Union[NumericValue, float], - args: Sequence[Union[NumericValue, float]], + node: Union[NumericValue, float], + args: Sequence[Union[NumericValue, float]], info: BinaryMultiplicationInfo, ): return pe.tan(args[0]) def handle_asin( - node: Union[NumericValue, float], - args: Sequence[Union[NumericValue, float]], + node: Union[NumericValue, float], + args: Sequence[Union[NumericValue, float]], info: BinaryMultiplicationInfo, ): return pe.asin(args[0]) def handle_acos( - node: Union[NumericValue, float], - args: Sequence[Union[NumericValue, float]], + node: Union[NumericValue, float], + args: Sequence[Union[NumericValue, float]], info: BinaryMultiplicationInfo, ): return pe.acos(args[0]) def handle_atan( - node: Union[NumericValue, float], - args: Sequence[Union[NumericValue, float]], + node: Union[NumericValue, float], + args: Sequence[Union[NumericValue, float]], info: BinaryMultiplicationInfo, ): return pe.atan(args[0]) def handle_sqrt( - node: Union[NumericValue, float], - args: Sequence[Union[NumericValue, float]], + node: Union[NumericValue, float], + args: Sequence[Union[NumericValue, float]], info: BinaryMultiplicationInfo, ): return pe.sqrt(args[0]) def handle_abs( - node: Union[NumericValue, float], - args: Sequence[Union[NumericValue, float]], + node: Union[NumericValue, float], + args: Sequence[Union[NumericValue, float]], info: BinaryMultiplicationInfo, ): return pe.abs(args[0]) def handle_div( - node: Union[NumericValue, float], - args: Sequence[Union[NumericValue, float]], + node: Union[NumericValue, float], + args: Sequence[Union[NumericValue, float]], info: BinaryMultiplicationInfo, ): x, y = args @@ -219,25 +227,25 @@ def handle_div( def handle_pow( - node: Union[NumericValue, float], - args: Sequence[Union[NumericValue, float]], + node: Union[NumericValue, float], + args: Sequence[Union[NumericValue, float]], info: BinaryMultiplicationInfo, ): x, y = args - return x ** y + return x**y def handle_negation( - node: Union[NumericValue, float], - args: Sequence[Union[NumericValue, float]], + node: Union[NumericValue, float], + args: Sequence[Union[NumericValue, float]], info: BinaryMultiplicationInfo, ): return -args[0] def handle_named_expression( - node: Union[NumericValue, float], - args: Sequence[Union[NumericValue, float]], + node: Union[NumericValue, float], + args: Sequence[Union[NumericValue, float]], info: BinaryMultiplicationInfo, ): return args[0] @@ -258,8 +266,8 @@ def handle_named_expression( def handle_unary( - node: Union[NumericValue, float], - args: Sequence[Union[NumericValue, float]], + node: Union[NumericValue, float], + args: Sequence[Union[NumericValue, float]], info: BinaryMultiplicationInfo, ): return unary_handlers[node.getname()](node, args, info) @@ -305,7 +313,7 @@ def exitNode(self, node, data): def reformulate_binary_multiplication(m: _BlockData): """ The goal of this function is to replace f(x) * y = 0 with - a McCormick relaxation when y is binary (in which case the + a McCormick relaxation when y is binary (in which case the McCormick relaxation is equivalent). """ r = pe.ConcreteModel() @@ -317,7 +325,7 @@ def reformulate_binary_multiplication(m: _BlockData): info = walker.info for c in iterators.nonrelaxation_component_data_objects( - m, pe.Constraint, active=True, descend_into=True, + m, pe.Constraint, active=True, descend_into=True ): repn = generate_standard_repn(c.body, compute_values=True, quadratic=False) if repn.nonlinear_expr is None: @@ -332,7 +340,7 @@ def reformulate_binary_multiplication(m: _BlockData): r.cons.add((c.lb, new_body, c.ub)) for obj in iterators.nonrelaxation_component_data_objects( - m, pe.Objective, active=True, descend_into=True, + m, pe.Objective, active=True, descend_into=True ): repn = generate_standard_repn(obj.expr, compute_values=True, quadratic=False) if repn.nonlinear_expr is None: diff --git a/pyomo/contrib/coramin/heuristics/diving.py b/pyomo/contrib/coramin/heuristics/diving.py index ca8aea52ab5..18511473eaf 100644 --- a/pyomo/contrib/coramin/heuristics/diving.py +++ b/pyomo/contrib/coramin/heuristics/diving.py @@ -16,11 +16,15 @@ from pyomo.contrib.coramin.relaxations import iterators -def collect_vars(m: _BlockData) -> Tuple[List[_GeneralVarData], List[_GeneralVarData], List[_GeneralVarData]]: +def collect_vars( + m: _BlockData, +) -> Tuple[List[_GeneralVarData], List[_GeneralVarData], List[_GeneralVarData]]: binary_vars = ComponentSet() integer_vars = ComponentSet() all_vars = ComponentSet() - for c in m.component_data_objects(pe.Constraint, active=True, descend_into=pe.Block): + for c in m.component_data_objects( + pe.Constraint, active=True, descend_into=pe.Block + ): for v in identify_variables(c.body, include_fixed=False): all_vars.add(v) if v.is_binary(): @@ -38,7 +42,9 @@ def collect_vars(m: _BlockData) -> Tuple[List[_GeneralVarData], List[_GeneralVar return list(binary_vars), list(integer_vars), list(all_vars) -def relax_integers(binary_vars: Sequence[_GeneralVarData], integer_vars: Sequence[_GeneralVarData]): +def relax_integers( + binary_vars: Sequence[_GeneralVarData], integer_vars: Sequence[_GeneralVarData] +): for v in list(binary_vars) + list(integer_vars): lb, ub = v.bounds v.domain = pe.Reals @@ -46,7 +52,9 @@ def relax_integers(binary_vars: Sequence[_GeneralVarData], integer_vars: Sequenc v.setub(ub) -def restore_integers(binary_vars: Sequence[_GeneralVarData], integer_vars: Sequence[_GeneralVarData]): +def restore_integers( + binary_vars: Sequence[_GeneralVarData], integer_vars: Sequence[_GeneralVarData] +): for v in binary_vars: v.domain = pe.Binary for v in integer_vars: @@ -58,11 +66,15 @@ def __init__(self, m: _BlockData) -> None: super().__init__() binary_vars, integer_vars, all_vars = collect_vars(m) - self.relaxation = clone_shallow_active_flat(reformulate_binary_multiplication(m))[0] + self.relaxation = clone_shallow_active_flat( + reformulate_binary_multiplication(m) + )[0] orig_lbs = [v.lb for v in self.relaxation.vars] orig_ubs = [v.ub for v in self.relaxation.vars] - for r in iterators.relaxation_data_objects(self.relaxation, descend_into=True, active=True): + for r in iterators.relaxation_data_objects( + self.relaxation, descend_into=True, active=True + ): r.rebuild(build_nonlinear_constraint=True) tightener = IntervalTightener() tightener.config.deactivate_satisfied_constraints = False @@ -95,7 +107,7 @@ def __init__(self, m: _BlockData) -> None: def sense(self): return self._sense - + def bound(self): orig_lbs = [v.lb for v in self.relaxation.vars] orig_ubs = [v.ub for v in self.relaxation.vars] @@ -108,7 +120,9 @@ def bound(self): if v.ub is None or (ub is not None and ub < v.ub): v.setub(ub) - for r in iterators.relaxation_data_objects(self.relaxation, descend_into=True, active=True): + for r in iterators.relaxation_data_objects( + self.relaxation, descend_into=True, active=True + ): r.rebuild() for v, lb, ub in zip(self.relaxation.vars, orig_lbs, orig_ubs): @@ -117,7 +131,12 @@ def bound(self): opt = pe.SolverFactory('ipopt') try: - res = opt.solve(self.relaxation, skip_trivial_constraints=True, load_solutions=False, tee=False) + res = opt.solve( + self.relaxation, + skip_trivial_constraints=True, + load_solutions=False, + tee=False, + ) except: return self.infeasible_objective() if not pe.check_optimal_termination(res): @@ -129,7 +148,7 @@ def bound(self): else: ret = min(self.current_node.bound, ret) return ret - + def objective(self): unfixed_vars = [v for v in self.bin_and_int_vars if not v.is_fixed()] vals = [v.value for v in unfixed_vars] @@ -148,7 +167,12 @@ def objective(self): opt = pe.SolverFactory('ipopt') opt.options['max_iter'] = 300 try: - res = opt.solve(self.m, skip_trivial_constraints=True, load_solutions=False, tee=False) + res = opt.solve( + self.m, + skip_trivial_constraints=True, + load_solutions=False, + tee=False, + ) except: success = False @@ -167,7 +191,7 @@ def objective(self): # we have to restore the values so that branch() works properly v.set_value(val, skip_validation=True) return ret - + def get_state(self): xl = [math.ceil(v.lb) for v in self.bin_and_int_vars] xl = np.array(xl, dtype=int) @@ -176,7 +200,7 @@ def get_state(self): xu = np.array(xu, dtype=int) return xl, xu, None - + def save_state(self, node): node.state = self.get_state() @@ -199,7 +223,10 @@ def branch(self): return pybnb.Node() xl, xu, _ = self.get_state() - dist_list = [(abs(v.value - round(v.value)), ndx) for ndx, v in enumerate(self.bin_and_int_vars)] + dist_list = [ + (abs(v.value - round(v.value)), ndx) + for ndx, v in enumerate(self.bin_and_int_vars) + ] dist_list.sort(key=lambda i: i[0], reverse=True) ndx = dist_list[0][1] branching_var = self.bin_and_int_vars[ndx] @@ -220,32 +247,65 @@ def branch(self): yield child2 -def assert_feasible(m: _BlockData, var_list: Sequence[_GeneralVarData], feasibility_tol: float, integer_tol: float): - for c in m.component_data_objects(pe.Constraint, active=True, descend_into=pe.Block): +def assert_feasible( + m: _BlockData, + var_list: Sequence[_GeneralVarData], + feasibility_tol: float, + integer_tol: float, +): + for c in m.component_data_objects( + pe.Constraint, active=True, descend_into=pe.Block + ): body_val = pe.value(c.body) if c.lb is not None: - assert c.lb - feasibility_tol <= body_val or abs(c.lb - body_val)/abs(c.lb) <= feasibility_tol + assert ( + c.lb - feasibility_tol <= body_val + or abs(c.lb - body_val) / abs(c.lb) <= feasibility_tol + ) if c.ub is not None: - assert body_val <= c.ub + feasibility_tol or abs(c.ub - body_val)/abs(c.ub) <= feasibility_tol + assert ( + body_val <= c.ub + feasibility_tol + or abs(c.ub - body_val) / abs(c.ub) <= feasibility_tol + ) for v in var_list: val = v.value lb, ub = v.bounds if lb is not None: - assert lb - feasibility_tol <= val or abs(lb - val)/abs(lb) <= feasibility_tol + assert ( + lb - feasibility_tol <= val + or abs(lb - val) / abs(lb) <= feasibility_tol + ) if ub is not None: - assert val <= ub + feasibility_tol or abs(ub - val)/abs(ub) <= feasibility_tol + assert ( + val <= ub + feasibility_tol + or abs(ub - val) / abs(ub) <= feasibility_tol + ) if v.is_integer(): assert abs(val - round(val)) <= integer_tol -def run_diving_heuristic(m: _BlockData, feasibility_tol: float = 1e-6, integer_tol: float = 1e-4, time_limit: float = 300, node_limit: int = 1000): +def run_diving_heuristic( + m: _BlockData, + feasibility_tol: float = 1e-6, + integer_tol: float = 1e-4, + time_limit: float = 300, + node_limit: int = 1000, +): prob = DivingHeuristic(m) - res: pybnb.SolverResults = pybnb.solve(prob, queue_strategy=pybnb.QueueStrategy.bound, objective_stop=prob.infeasible_objective(), node_limit=node_limit, time_limit=time_limit) + res: pybnb.SolverResults = pybnb.solve( + prob, + queue_strategy=pybnb.QueueStrategy.bound, + objective_stop=prob.infeasible_objective(), + node_limit=node_limit, + time_limit=time_limit, + ) ss = pybnb.SolutionStatus if res.solution_status in {ss.feasible, ss.optimal}: best_obj = res.objective - best_sol: MutableMapping[_GeneralVarData, float] = ComponentMap(zip(prob.all_vars, res.best_node.state[2])) + best_sol: MutableMapping[_GeneralVarData, float] = ComponentMap( + zip(prob.all_vars, res.best_node.state[2]) + ) else: best_obj = None best_sol = None diff --git a/pyomo/contrib/coramin/heuristics/feasibility_pump.py b/pyomo/contrib/coramin/heuristics/feasibility_pump.py index efd8ee1ba91..03dc7d6f7b7 100644 --- a/pyomo/contrib/coramin/heuristics/feasibility_pump.py +++ b/pyomo/contrib/coramin/heuristics/feasibility_pump.py @@ -13,10 +13,14 @@ import numpy as np -def collect_integer_vars(m: _BlockData) -> Tuple[List[_GeneralVarData], List[_GeneralVarData]]: +def collect_integer_vars( + m: _BlockData, +) -> Tuple[List[_GeneralVarData], List[_GeneralVarData]]: binary_vars = ComponentSet() integer_vars = ComponentSet() - for c in m.component_data_objects(pe.Constraint, active=True, descend_into=pe.Block): + for c in m.component_data_objects( + pe.Constraint, active=True, descend_into=pe.Block + ): for v in identify_variables(c.body, include_fixed=False): if v.is_binary(): binary_vars.add(v) @@ -32,7 +36,9 @@ def collect_integer_vars(m: _BlockData) -> Tuple[List[_GeneralVarData], List[_Ge return list(binary_vars), list(integer_vars) -def relax_integers(binary_vars: Sequence[_GeneralVarData], integer_vars: Sequence[_GeneralVarData]): +def relax_integers( + binary_vars: Sequence[_GeneralVarData], integer_vars: Sequence[_GeneralVarData] +): for v in list(binary_vars) + list(integer_vars): lb, ub = v.bounds v.domain = pe.Reals @@ -40,14 +46,20 @@ def relax_integers(binary_vars: Sequence[_GeneralVarData], integer_vars: Sequenc v.setub(ub) -def restore_integers(binary_vars: Sequence[_GeneralVarData], integer_vars: Sequence[_GeneralVarData]): +def restore_integers( + binary_vars: Sequence[_GeneralVarData], integer_vars: Sequence[_GeneralVarData] +): for v in binary_vars: v.domain = pe.Binary for v in integer_vars: v.domain = pe.Integers -def check_feasible(binary_vars: Sequence[_GeneralVarData], integer_vars: Sequence[_GeneralVarData], integer_tol=1e-4): +def check_feasible( + binary_vars: Sequence[_GeneralVarData], + integer_vars: Sequence[_GeneralVarData], + integer_tol=1e-4, +): feas = True for v in list(binary_vars) + list(integer_vars): v_val = v.value @@ -57,7 +69,16 @@ def check_feasible(binary_vars: Sequence[_GeneralVarData], integer_vars: Sequenc break return feas -def run_feasibility_pump(m: _BlockData, nlp_solver: Solver, time_limit: float = math.inf, iter_limit=300, integer_tol=1e-4, use_fixing: bool = False, use_flip: bool = True): + +def run_feasibility_pump( + m: _BlockData, + nlp_solver: Solver, + time_limit: float = math.inf, + iter_limit=300, + integer_tol=1e-4, + use_fixing: bool = False, + use_flip: bool = True, +): t0 = time.time() binary_vars, integer_vars = collect_integer_vars(m) @@ -127,11 +148,14 @@ def run_feasibility_pump(m: _BlockData, nlp_solver: Solver, time_limit: float = integer_vars[_ndx].fix(target_integer_vals[_ndx]) if last_target_binary_vals is not None and use_flip: - if target_binary_vals == last_target_binary_vals and target_integer_vals == last_target_integer_vals: + if ( + target_binary_vals == last_target_binary_vals + and target_integer_vals == last_target_integer_vals + ): print('flipping') - T = math.floor(0.5*(len(binary_vars) + len(integer_vars))) + T = math.floor(0.5 * (len(binary_vars) + len(integer_vars))) T = 10 - num_flip = random.randint(math.floor(0.5*T), math.ceil(1.5*T)) + num_flip = random.randint(math.floor(0.5 * T), math.ceil(1.5 * T)) dist_list = list() ndx = 0 for v, val in zip(binary_vars, target_binary_vals): @@ -166,10 +190,10 @@ def run_feasibility_pump(m: _BlockData, nlp_solver: Solver, time_limit: float = obj_expr += v else: assert val == 1 - obj_expr += (1 - v) + obj_expr += 1 - v for v, val in zip(integer_vars, target_integer_vals): - obj_expr += (v - val)**2 - setattr(m, new_obj_name, pe.Objective(expr=obj_expr)) + obj_expr += (v - val) ** 2 + setattr(m, new_obj_name, pe.Objective(expr=obj_expr)) res = nlp_solver.solve(m) if res.best_feasible_objective is None: @@ -177,7 +201,7 @@ def run_feasibility_pump(m: _BlockData, nlp_solver: Solver, time_limit: float = break res.solution_loader.load_vars([v for v in binary_vars if not v.is_fixed()]) res.solution_loader.load_vars([v for v in integer_vars if not v.is_fixed()]) - + is_feas = check_feasible(binary_vars, integer_vars, integer_tol) if is_feas: feasible_results = res diff --git a/pyomo/contrib/coramin/heuristics/tests/test_heuristics.py b/pyomo/contrib/coramin/heuristics/tests/test_heuristics.py index 050740d30e4..68f80c87a66 100644 --- a/pyomo/contrib/coramin/heuristics/tests/test_heuristics.py +++ b/pyomo/contrib/coramin/heuristics/tests/test_heuristics.py @@ -12,7 +12,7 @@ def test_diving_1(self): m.z2 = pe.Var(domain=pe.Binary) m.obj = pe.Objective(expr=m.x**2 + m.y**2) m.c1 = pe.Constraint(expr=(m.y - pe.exp(m.x)) * m.z1 >= 0) - m.c2 = pe.Constraint(expr=(m.y - (m.x - 1)**2) * m.z1 >= 0) + m.c2 = pe.Constraint(expr=(m.y - (m.x - 1) ** 2) * m.z1 >= 0) m.c3 = pe.Constraint(expr=(m.y - m.x - 2) * m.z2 >= 0) m.c4 = pe.Constraint(expr=(m.y + m.x - 2) * m.z2 >= 0) m.c5 = pe.Constraint(expr=m.z1 + m.z2 == 1) diff --git a/pyomo/contrib/coramin/relaxations/alphabb.py b/pyomo/contrib/coramin/relaxations/alphabb.py index 8fcd273b9b4..eba240dc3ed 100644 --- a/pyomo/contrib/coramin/relaxations/alphabb.py +++ b/pyomo/contrib/coramin/relaxations/alphabb.py @@ -1,8 +1,6 @@ from pyomo.contrib.coramin.utils.coramin_enums import EigenValueBounder, RelaxationSide from pyomo.contrib.coramin.relaxations.custom_block import declare_custom_block -from pyomo.contrib.coramin.relaxations.relaxations_base import ( - BaseRelaxationData, -) +from pyomo.contrib.coramin.relaxations.relaxations_base import BaseRelaxationData from pyomo.contrib.coramin.relaxations.hessian import Hessian from typing import Optional, Tuple from pyomo.core.base.var import _GeneralVarData diff --git a/pyomo/contrib/coramin/relaxations/auto_relax.py b/pyomo/contrib/coramin/relaxations/auto_relax.py index b30114f5460..90c713561b3 100644 --- a/pyomo/contrib/coramin/relaxations/auto_relax.py +++ b/pyomo/contrib/coramin/relaxations/auto_relax.py @@ -19,10 +19,7 @@ PWArctanRelaxation, ) from .mccormick import PWMcCormickRelaxation -from pyomo.contrib.coramin.utils.coramin_enums import ( - RelaxationSide, - FunctionShape, -) +from pyomo.contrib.coramin.utils.coramin_enums import RelaxationSide, FunctionShape from pyomo.core.base.expression import _GeneralExpressionData, SimpleExpression from pyomo.repn.standard_repn import generate_standard_repn from .iterators import relaxation_data_objects @@ -902,44 +899,44 @@ def _relax_leaf_to_root_GeneralExpression( _relax_leaf_to_root_map = dict() -_relax_leaf_to_root_map[ - numeric_expr.ProductExpression -] = _relax_leaf_to_root_ProductExpression +_relax_leaf_to_root_map[numeric_expr.ProductExpression] = ( + _relax_leaf_to_root_ProductExpression +) _relax_leaf_to_root_map[numeric_expr.SumExpression] = _relax_leaf_to_root_SumExpression -_relax_leaf_to_root_map[ - numeric_expr.LinearExpression -] = _relax_leaf_to_root_SumExpression -_relax_leaf_to_root_map[ - numeric_expr.MonomialTermExpression -] = _relax_leaf_to_root_ProductExpression -_relax_leaf_to_root_map[ - numeric_expr.NegationExpression -] = _relax_leaf_to_root_NegationExpression +_relax_leaf_to_root_map[numeric_expr.LinearExpression] = ( + _relax_leaf_to_root_SumExpression +) +_relax_leaf_to_root_map[numeric_expr.MonomialTermExpression] = ( + _relax_leaf_to_root_ProductExpression +) +_relax_leaf_to_root_map[numeric_expr.NegationExpression] = ( + _relax_leaf_to_root_NegationExpression +) _relax_leaf_to_root_map[numeric_expr.PowExpression] = _relax_leaf_to_root_PowExpression -_relax_leaf_to_root_map[ - numeric_expr.DivisionExpression -] = _relax_leaf_to_root_DivisionExpression -_relax_leaf_to_root_map[ - numeric_expr.UnaryFunctionExpression -] = _relax_leaf_to_root_UnaryFunctionExpression -_relax_leaf_to_root_map[ - numeric_expr.NPV_ProductExpression -] = _relax_leaf_to_root_ProductExpression -_relax_leaf_to_root_map[ - numeric_expr.NPV_SumExpression -] = _relax_leaf_to_root_SumExpression -_relax_leaf_to_root_map[ - numeric_expr.NPV_NegationExpression -] = _relax_leaf_to_root_NegationExpression -_relax_leaf_to_root_map[ - numeric_expr.NPV_PowExpression -] = _relax_leaf_to_root_PowExpression -_relax_leaf_to_root_map[ - numeric_expr.NPV_DivisionExpression -] = _relax_leaf_to_root_DivisionExpression -_relax_leaf_to_root_map[ - numeric_expr.NPV_UnaryFunctionExpression -] = _relax_leaf_to_root_UnaryFunctionExpression +_relax_leaf_to_root_map[numeric_expr.DivisionExpression] = ( + _relax_leaf_to_root_DivisionExpression +) +_relax_leaf_to_root_map[numeric_expr.UnaryFunctionExpression] = ( + _relax_leaf_to_root_UnaryFunctionExpression +) +_relax_leaf_to_root_map[numeric_expr.NPV_ProductExpression] = ( + _relax_leaf_to_root_ProductExpression +) +_relax_leaf_to_root_map[numeric_expr.NPV_SumExpression] = ( + _relax_leaf_to_root_SumExpression +) +_relax_leaf_to_root_map[numeric_expr.NPV_NegationExpression] = ( + _relax_leaf_to_root_NegationExpression +) +_relax_leaf_to_root_map[numeric_expr.NPV_PowExpression] = ( + _relax_leaf_to_root_PowExpression +) +_relax_leaf_to_root_map[numeric_expr.NPV_DivisionExpression] = ( + _relax_leaf_to_root_DivisionExpression +) +_relax_leaf_to_root_map[numeric_expr.NPV_UnaryFunctionExpression] = ( + _relax_leaf_to_root_UnaryFunctionExpression +) _relax_leaf_to_root_map[_GeneralExpressionData] = _relax_leaf_to_root_GeneralExpression _relax_leaf_to_root_map[SimpleExpression] = _relax_leaf_to_root_GeneralExpression @@ -1102,44 +1099,44 @@ def _relax_root_to_leaf_GeneralExpression(node, relaxation_side_map): _relax_root_to_leaf_map = dict() -_relax_root_to_leaf_map[ - numeric_expr.ProductExpression -] = _relax_root_to_leaf_ProductExpression +_relax_root_to_leaf_map[numeric_expr.ProductExpression] = ( + _relax_root_to_leaf_ProductExpression +) _relax_root_to_leaf_map[numeric_expr.SumExpression] = _relax_root_to_leaf_SumExpression -_relax_root_to_leaf_map[ - numeric_expr.LinearExpression -] = _relax_root_to_leaf_SumExpression -_relax_root_to_leaf_map[ - numeric_expr.MonomialTermExpression -] = _relax_root_to_leaf_ProductExpression -_relax_root_to_leaf_map[ - numeric_expr.NegationExpression -] = _relax_root_to_leaf_NegationExpression +_relax_root_to_leaf_map[numeric_expr.LinearExpression] = ( + _relax_root_to_leaf_SumExpression +) +_relax_root_to_leaf_map[numeric_expr.MonomialTermExpression] = ( + _relax_root_to_leaf_ProductExpression +) +_relax_root_to_leaf_map[numeric_expr.NegationExpression] = ( + _relax_root_to_leaf_NegationExpression +) _relax_root_to_leaf_map[numeric_expr.PowExpression] = _relax_root_to_leaf_PowExpression -_relax_root_to_leaf_map[ - numeric_expr.DivisionExpression -] = _relax_root_to_leaf_DivisionExpression -_relax_root_to_leaf_map[ - numeric_expr.UnaryFunctionExpression -] = _relax_root_to_leaf_UnaryFunctionExpression -_relax_root_to_leaf_map[ - numeric_expr.NPV_ProductExpression -] = _relax_root_to_leaf_ProductExpression -_relax_root_to_leaf_map[ - numeric_expr.NPV_SumExpression -] = _relax_root_to_leaf_SumExpression -_relax_root_to_leaf_map[ - numeric_expr.NPV_NegationExpression -] = _relax_root_to_leaf_NegationExpression -_relax_root_to_leaf_map[ - numeric_expr.NPV_PowExpression -] = _relax_root_to_leaf_PowExpression -_relax_root_to_leaf_map[ - numeric_expr.NPV_DivisionExpression -] = _relax_root_to_leaf_DivisionExpression -_relax_root_to_leaf_map[ - numeric_expr.NPV_UnaryFunctionExpression -] = _relax_root_to_leaf_UnaryFunctionExpression +_relax_root_to_leaf_map[numeric_expr.DivisionExpression] = ( + _relax_root_to_leaf_DivisionExpression +) +_relax_root_to_leaf_map[numeric_expr.UnaryFunctionExpression] = ( + _relax_root_to_leaf_UnaryFunctionExpression +) +_relax_root_to_leaf_map[numeric_expr.NPV_ProductExpression] = ( + _relax_root_to_leaf_ProductExpression +) +_relax_root_to_leaf_map[numeric_expr.NPV_SumExpression] = ( + _relax_root_to_leaf_SumExpression +) +_relax_root_to_leaf_map[numeric_expr.NPV_NegationExpression] = ( + _relax_root_to_leaf_NegationExpression +) +_relax_root_to_leaf_map[numeric_expr.NPV_PowExpression] = ( + _relax_root_to_leaf_PowExpression +) +_relax_root_to_leaf_map[numeric_expr.NPV_DivisionExpression] = ( + _relax_root_to_leaf_DivisionExpression +) +_relax_root_to_leaf_map[numeric_expr.NPV_UnaryFunctionExpression] = ( + _relax_root_to_leaf_UnaryFunctionExpression +) _relax_root_to_leaf_map[_GeneralExpressionData] = _relax_root_to_leaf_GeneralExpression _relax_root_to_leaf_map[SimpleExpression] = _relax_root_to_leaf_GeneralExpression @@ -1316,9 +1313,7 @@ def _relax_cloned_model(m): relaxation.rebuild() -def relax( - model, -): +def relax(model): """ Create a convex relaxation of the model. diff --git a/pyomo/contrib/coramin/relaxations/mccormick.py b/pyomo/contrib/coramin/relaxations/mccormick.py index 15b82545569..79e5c9bb4de 100644 --- a/pyomo/contrib/coramin/relaxations/mccormick.py +++ b/pyomo/contrib/coramin/relaxations/mccormick.py @@ -250,7 +250,13 @@ def remove_relaxation(self): self._remove_relaxation() def rebuild(self, build_nonlinear_constraint=False, ensure_oa_at_vertices=True): - build_nonlinear_constraint = build_nonlinear_constraint or self._x1.is_fixed() or self._x2.is_fixed() or (self._x1.lb == self._x1.ub and self._x1.lb is not None) or (self._x2.lb == self._x2.ub and self._x2.lb is not None) + build_nonlinear_constraint = ( + build_nonlinear_constraint + or self._x1.is_fixed() + or self._x2.is_fixed() + or (self._x1.lb == self._x1.ub and self._x1.lb is not None) + or (self._x2.lb == self._x2.ub and self._x2.lb is not None) + ) super(PWMcCormickRelaxationData, self).rebuild( build_nonlinear_constraint=build_nonlinear_constraint, ensure_oa_at_vertices=ensure_oa_at_vertices, diff --git a/pyomo/contrib/coramin/relaxations/multivariate.py b/pyomo/contrib/coramin/relaxations/multivariate.py index e7ee76b25fa..3a2275fe6a6 100644 --- a/pyomo/contrib/coramin/relaxations/multivariate.py +++ b/pyomo/contrib/coramin/relaxations/multivariate.py @@ -1,8 +1,6 @@ from pyomo.contrib.coramin.utils.coramin_enums import RelaxationSide, FunctionShape from pyomo.contrib.coramin.relaxations.custom_block import declare_custom_block -from pyomo.contrib.coramin.relaxations.relaxations_base import ( - BaseRelaxationData, -) +from pyomo.contrib.coramin.relaxations.relaxations_base import BaseRelaxationData from pyomo.core.expr.visitor import identify_variables import math import pyomo.environ as pe diff --git a/pyomo/contrib/coramin/relaxations/relaxations_base.py b/pyomo/contrib/coramin/relaxations/relaxations_base.py index 55abe4ab872..6baf3fb1e40 100644 --- a/pyomo/contrib/coramin/relaxations/relaxations_base.py +++ b/pyomo/contrib/coramin/relaxations/relaxations_base.py @@ -133,7 +133,11 @@ def _check_cut( cut.constant._value -= safety_tol else: cut.constant._value += safety_tol - if type(cut.constant.value) is complex or not math.isfinite(cut.constant.value) or abs(cut.constant.value) >= too_large: + if ( + type(cut.constant.value) is complex + or not math.isfinite(cut.constant.value) + or abs(cut.constant.value) >= too_large + ): res = (False, None, cut.constant.value, None) return res @@ -313,7 +317,9 @@ def rebuild(self, build_nonlinear_constraint=False, ensure_oa_at_vertices=True): self._cuts = IndexedConstraint(pe.Any) if self._oa_params is None: del self._oa_params - self._oa_params = IndexedParam(pe.Any, mutable=True, initialize=0, within=pe.Any) + self._oa_params = IndexedParam( + pe.Any, mutable=True, initialize=0, within=pe.Any + ) self.clean_oa_points(ensure_oa_at_vertices=ensure_oa_at_vertices) self._update_oa_cuts() else: @@ -573,7 +579,9 @@ def clear_oa_points(self): self._current_param_index = 0 if self._oa_params is not None: del self._oa_params - self._oa_params = pe.Param(pe.Any, mutable=True, initialize=0, within=pe.Any) + self._oa_params = pe.Param( + pe.Any, mutable=True, initialize=0, within=pe.Any + ) if self._cuts is not None: del self._cuts self._cuts = pe.Constraint(pe.Any) diff --git a/pyomo/contrib/coramin/relaxations/tests/test_auto_relax.py b/pyomo/contrib/coramin/relaxations/tests/test_auto_relax.py index da48b6a296b..5ab3918f277 100644 --- a/pyomo/contrib/coramin/relaxations/tests/test_auto_relax.py +++ b/pyomo/contrib/coramin/relaxations/tests/test_auto_relax.py @@ -686,14 +686,16 @@ def test_sqrt(self): self.assertEqual(len(rels), 2) rel0 = m.relaxations.rel0 # log rel1 = m.relaxations.rel1 # sqrt - self.assertEqual(sympyify_expression(rel0.get_rhs_expr() - pe.log(orig.x))[1], 0) + self.assertEqual( + sympyify_expression(rel0.get_rhs_expr() - pe.log(orig.x))[1], 0 + ) self.assertEqual( sympyify_expression(rel1.get_rhs_expr() - m.aux_vars[3] ** 0.5)[1], 0 ) self.assertEqual( - sympyify_expression(m.linear.cons[1].body - m.aux_vars[3] + 2 * m.aux_vars[1])[ - 1 - ], + sympyify_expression( + m.linear.cons[1].body - m.aux_vars[3] + 2 * m.aux_vars[1] + )[1], 0, ) self.assertEqual( @@ -1150,7 +1152,9 @@ def helper(self, func, param_val): self.assertEqual(len(rels), 1) r = rels[0] self.assertIsInstance(r, coramin.relaxations.PWXSquaredRelaxationData) - assertExpressionsEqual(self, m.linear.cons[1].body, orig.aux - func(param_val)*m.aux_vars[1]) + assertExpressionsEqual( + self, m.linear.cons[1].body, orig.aux - func(param_val) * m.aux_vars[1] + ) self.assertEqual(m.linear.cons[1].lb, 0) self.assertEqual(m.linear.cons[1].ub, 0) diff --git a/pyomo/contrib/coramin/relaxations/tests/test_relaxations.py b/pyomo/contrib/coramin/relaxations/tests/test_relaxations.py index 155b5d468de..423498b12dd 100644 --- a/pyomo/contrib/coramin/relaxations/tests/test_relaxations.py +++ b/pyomo/contrib/coramin/relaxations/tests/test_relaxations.py @@ -1118,9 +1118,7 @@ def test_multivariate_convex(self): m = self.get_base_pyomo_model() m.rel = coramin.relaxations.MultivariateRelaxation() m.rel.build( - aux_var=m.z, - shape=coramin.FunctionShape.CONVEX, - f_x_expr=m.x**2 + m.y**2, + aux_var=m.z, shape=coramin.FunctionShape.CONVEX, f_x_expr=m.x**2 + m.y**2 ) e = m.x**2 + m.y**2 self.options_switching_helper(m.rel) @@ -1147,9 +1145,7 @@ def test_multivariate_concave(self): m = self.get_base_pyomo_model() m.rel = coramin.relaxations.MultivariateRelaxation() m.rel.build( - aux_var=m.z, - shape=coramin.FunctionShape.CONCAVE, - f_x_expr=-m.x**2 - m.y**2, + aux_var=m.z, shape=coramin.FunctionShape.CONCAVE, f_x_expr=-m.x**2 - m.y**2 ) e = -m.x**2 - m.y**2 self.valid_relaxation_helper(m, m.rel, e, 10, False, True) diff --git a/pyomo/contrib/coramin/relaxations/univariate.py b/pyomo/contrib/coramin/relaxations/univariate.py index a0d0b950051..4a839953552 100644 --- a/pyomo/contrib/coramin/relaxations/univariate.py +++ b/pyomo/contrib/coramin/relaxations/univariate.py @@ -296,11 +296,9 @@ def pw_sin_relaxation( if xub > np.pi / 2.0: return - ( - OE_tangent_x, - OE_tangent_slope, - OE_tangent_intercept, - ) = _compute_sine_overestimator_tangent_point(xlb) + (OE_tangent_x, OE_tangent_slope, OE_tangent_intercept) = ( + _compute_sine_overestimator_tangent_point(xlb) + ) ( under_estimator_tangent_x, under_estimator_tangent_slope, @@ -375,11 +373,9 @@ def pw_sin_relaxation( b.w[i] <= slope * b.x[i] + (intercept + safety_tol) * b.lam[i] ) elif (x0 < 0) and (x1 > 0): - ( - tangent_x, - tangent_slope, - tangent_intercept, - ) = _compute_sine_overestimator_tangent_point(x0) + (tangent_x, tangent_slope, tangent_intercept) = ( + _compute_sine_overestimator_tangent_point(x0) + ) if tangent_x <= x1: b.overestimators.add( b.w[i] @@ -418,11 +414,9 @@ def pw_sin_relaxation( b.w[i] >= slope * b.x[i] + (intercept - safety_tol) * b.lam[i] ) elif (x1 > 0) and (x0 < 0): - ( - tangent_x, - tangent_slope, - tangent_intercept, - ) = _compute_sine_underestimator_tangent_point(x1) + (tangent_x, tangent_slope, tangent_intercept) = ( + _compute_sine_underestimator_tangent_point(x1) + ) if tangent_x >= x0: b.underestimators.add( b.w[i] @@ -494,11 +488,9 @@ def pw_arctan_relaxation( if xlb == -math.inf or xub == math.inf: return - ( - OE_tangent_x, - OE_tangent_slope, - OE_tangent_intercept, - ) = _compute_arctan_overestimator_tangent_point(xlb) + (OE_tangent_x, OE_tangent_slope, OE_tangent_intercept) = ( + _compute_arctan_overestimator_tangent_point(xlb) + ) ( under_estimator_tangent_x, under_estimator_tangent_slope, @@ -573,11 +565,9 @@ def pw_arctan_relaxation( b.w[i] <= slope * b.x[i] + (intercept + safety_tol) * b.lam[i] ) elif (x0 < 0) and (x1 > 0): - ( - tangent_x, - tangent_slope, - tangent_intercept, - ) = _compute_arctan_overestimator_tangent_point(x0) + (tangent_x, tangent_slope, tangent_intercept) = ( + _compute_arctan_overestimator_tangent_point(x0) + ) if tangent_x <= x1: b.overestimators.add( b.w[i] @@ -616,11 +606,9 @@ def pw_arctan_relaxation( b.w[i] >= slope * b.x[i] + (intercept - safety_tol) * b.lam[i] ) elif (x1 > 0) and (x0 < 0): - ( - tangent_x, - tangent_slope, - tangent_intercept, - ) = _compute_arctan_underestimator_tangent_point(x1) + (tangent_x, tangent_slope, tangent_intercept) = ( + _compute_arctan_underestimator_tangent_point(x1) + ) if tangent_x >= x0: b.underestimators.add( b.w[i] diff --git a/pyomo/contrib/coramin/utils/compare_models.py b/pyomo/contrib/coramin/utils/compare_models.py index 71e6395b23b..8c5f526dac6 100644 --- a/pyomo/contrib/coramin/utils/compare_models.py +++ b/pyomo/contrib/coramin/utils/compare_models.py @@ -35,11 +35,15 @@ def _attempt_presolve(m, vars_to_presolve): continue if not c.active: continue - repn: StandardRepn = generate_standard_repn(c.body - c.lb, compute_values=True, quadratic=False) + repn: StandardRepn = generate_standard_repn( + c.body - c.lb, compute_values=True, quadratic=False + ) lin_vars = ComponentSet(repn.linear_vars) nonlin_vars = ComponentSet(repn.nonlinear_vars) if v in lin_vars and v not in nonlin_vars: - n_vars = len(ComponentSet(list(repn.linear_vars) + list(repn.nonlinear_vars))) + n_vars = len( + ComponentSet(list(repn.linear_vars) + list(repn.nonlinear_vars)) + ) if density is None or n_vars < density: v_expr = -repn.constant for coef, other in zip(repn.linear_coefs, repn.linear_vars): @@ -52,25 +56,31 @@ def _attempt_presolve(m, vars_to_presolve): v_expr /= v_coef v_con = c v_repn = repn - v_vars = ComponentSet([i for i in v_repn.linear_vars if i in var_to_con_map]) - v_vars.update([i for i in v_repn.nonlinear_vars if i in var_to_con_map]) + v_vars = ComponentSet( + [i for i in v_repn.linear_vars if i in var_to_con_map] + ) + v_vars.update( + [i for i in v_repn.nonlinear_vars if i in var_to_con_map] + ) v_vars.remove(v) density = n_vars if v_expr is None: return False - + v_con.deactivate() - + if v.lb is not None or v.ub is not None: new_con = bound_cons.add((v.lb, v_expr, v.ub)) for _v in v_vars: var_to_con_map[_v].add(new_con) - + for c in con_list: if c is v_con: continue sub_map = {id(v): v_expr} - new_body = simplify_expr(replace_expressions(c.body, substitution_map=sub_map)) + new_body = simplify_expr( + replace_expressions(c.body, substitution_map=sub_map) + ) c.set_value((c.lb, new_body, c.ub)) for _v in v_vars: var_to_con_map[_v].add(c) @@ -78,7 +88,13 @@ def _attempt_presolve(m, vars_to_presolve): return True -def is_relaxation(a: _BlockData, b: _BlockData, opt: appsi.base.Solver, feasibility_tol: float = 1e-6, bigM: Optional[float] = None): +def is_relaxation( + a: _BlockData, + b: _BlockData, + opt: appsi.base.Solver, + feasibility_tol: float = 1e-6, + bigM: Optional[float] = None, +): """ Returns True if every feasible point in b is feasible for a (a is a relaxation of b) @@ -96,7 +112,9 @@ def is_relaxation(a: _BlockData, b: _BlockData, opt: appsi.base.Solver, feasibil if len(vars_to_presolve) > 0: a = clone_shallow_active_flat(a)[0] if not _attempt_presolve(a, vars_to_presolve): - raise RuntimeError('a has variables that b does not, which makes the following analysis invalid') + raise RuntimeError( + 'a has variables that b does not, which makes the following analysis invalid' + ) m = clone_shallow_active_flat(b)[0] if hasattr(m.linear, 'obj'): @@ -146,7 +164,13 @@ def is_relaxation(a: _BlockData, b: _BlockData, opt: appsi.base.Solver, feasibil return passed -def is_equivalent(a: _BlockData, b: _BlockData, opt: appsi.base.Solver, feasibility_tol: float = 1e-6, bigM: Optional[float] = None): +def is_equivalent( + a: _BlockData, + b: _BlockData, + opt: appsi.base.Solver, + feasibility_tol: float = 1e-6, + bigM: Optional[float] = None, +): """ Returns True if the feasible regions of a and b are the same a and b should share variables diff --git a/pyomo/contrib/coramin/utils/tests/test_compare_models.py b/pyomo/contrib/coramin/utils/tests/test_compare_models.py index d1b9cd64b9c..669c98d9bb8 100644 --- a/pyomo/contrib/coramin/utils/tests/test_compare_models.py +++ b/pyomo/contrib/coramin/utils/tests/test_compare_models.py @@ -1,7 +1,11 @@ import pyomo.environ as pe from pyomo.contrib import appsi from pyomo.common import unittest -from pyomo.contrib.coramin.utils.compare_models import is_relaxation, is_equivalent, _attempt_presolve +from pyomo.contrib.coramin.utils.compare_models import ( + is_relaxation, + is_equivalent, + _attempt_presolve, +) from pyomo.core.expr.compare import assertExpressionsEqual, compare_expressions @@ -36,7 +40,7 @@ def _get_model(self): m.c3 = pe.Constraint(expr=m.x3 + m.x4 == 1) m.c4 = pe.Constraint(expr=m.x4 == 1) return m - + def _compare_expressions(self, got, options): success = False for exp in options: @@ -53,14 +57,18 @@ def test_presolve1(self): self.assertTrue(m.c2.active) self.assertFalse(m.c3.active) self.assertTrue(m.c4.active) - self.assertTrue(self._compare_expressions(m.c1.body, [m.x1 + m.x2 + 1, m.x2 + m.x1 + 1])) + self.assertTrue( + self._compare_expressions(m.c1.body, [m.x1 + m.x2 + 1, m.x2 + m.x1 + 1]) + ) self.assertTrue(self._compare_expressions(m.c2.body, [m.x2 + 1])) self.assertEqual(m.c1.lb, 1) self.assertEqual(m.c1.ub, 1) self.assertEqual(m.c2.lb, 1) self.assertEqual(m.c2.ub, 1) self.assertEqual(len(m.bound_constraints), 1) - self.assertTrue(self._compare_expressions(m.bound_constraints[1].body, [1 - m.x4])) + self.assertTrue( + self._compare_expressions(m.bound_constraints[1].body, [1 - m.x4]) + ) self.assertEqual(m.bound_constraints[1].lb, -3) self.assertEqual(m.bound_constraints[1].ub, 3) @@ -72,7 +80,9 @@ def test_presolve2(self): self.assertTrue(m.c2.active) self.assertFalse(m.c3.active) self.assertFalse(m.c4.active) - self.assertTrue(self._compare_expressions(m.c1.body, [m.x1 + m.x2 + 1, m.x2 + m.x1 + 1])) + self.assertTrue( + self._compare_expressions(m.c1.body, [m.x1 + m.x2 + 1, m.x2 + m.x1 + 1]) + ) self.assertTrue(self._compare_expressions(m.c2.body, [m.x2 + 1])) self.assertEqual(m.c1.lb, 1) self.assertEqual(m.c1.ub, 1) From a2a6f2b7845e142b1161a04e175f015259f32f57 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Sat, 24 Feb 2024 22:18:25 -0700 Subject: [PATCH 104/128] fix typo --- pyomo/contrib/coramin/algorithms/bnb/bnb.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/coramin/algorithms/bnb/bnb.py b/pyomo/contrib/coramin/algorithms/bnb/bnb.py index aa2fb1f84f7..3bb7fb87873 100644 --- a/pyomo/contrib/coramin/algorithms/bnb/bnb.py +++ b/pyomo/contrib/coramin/algorithms/bnb/bnb.py @@ -462,7 +462,7 @@ def branch(self): xl = ns.lbs xu = ns.ubs - # relaod the solution to the relaxation to make sure branching happens correctly + # reload the solution to the relaxation to make sure branching happens correctly for v, val in self.relaxation_solution.items(): v.set_value(val, skip_validation=True) From 19f0c5d29b8117c268e58fe52e9109e31f38d4e5 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Sat, 24 Feb 2024 22:23:12 -0700 Subject: [PATCH 105/128] update optional requirements --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 70c1626a650..cdb7e2f6c23 100644 --- a/setup.py +++ b/setup.py @@ -285,6 +285,7 @@ def __ne__(self, other): 'sympy', # differentiation 'xlrd', # dataportals 'z3-solver', # community_detection + 'pybnb', # coramin # # subprocess output is merged more reliably if # 'PeekNamedPipe' is available from pywin32 From b88cfd6e356b270a3b962f7a208eaeb17350e779 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Sat, 24 Feb 2024 22:29:30 -0700 Subject: [PATCH 106/128] update imports --- pyomo/contrib/coramin/domain_reduction/tests/test_dbt.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/pyomo/contrib/coramin/domain_reduction/tests/test_dbt.py b/pyomo/contrib/coramin/domain_reduction/tests/test_dbt.py index b771f732464..ad04163329e 100644 --- a/pyomo/contrib/coramin/domain_reduction/tests/test_dbt.py +++ b/pyomo/contrib/coramin/domain_reduction/tests/test_dbt.py @@ -21,13 +21,6 @@ from networkx import is_bipartite from pyomo.common.collections import ComponentSet from networkx import Graph -from pyomo.core.expr.visitor import identify_variables -from pyomo.core.expr import differentiate -from egret.thirdparty.get_pglib_opf import get_pglib_opf -from egret.data.model_data import ModelData -from egret.models.acopf import create_psv_acopf_model -import os -from pyomo.contrib.coramin.utils.pyomo_utils import get_objective import filecmp from pyomo.contrib import appsi import pytest From 7f62934e8c16b04f6a82c59020b951898efc96b7 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Sat, 24 Feb 2024 22:47:34 -0700 Subject: [PATCH 107/128] update GHA --- .github/workflows/test_branches.yml | 2 ++ .github/workflows/test_pr_and_main.yml | 2 ++ setup.py | 1 - 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 1d3c4cf574c..9f6af82a52e 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -294,6 +294,8 @@ jobs: || echo "WARNING: Gurobi is not available" python -m pip install --cache-dir cache/pip xpress \ || echo "WARNING: Xpress Community Edition is not available" + python -m pip install --cache-dir cache/pip pybnb \ + || echo "WARNING: pybnb is not available" if [[ ${{matrix.python}} == pypy* ]]; then echo "skipping wntr for pypy" else diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index bf43cee3bd5..1ce5f673986 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -316,6 +316,8 @@ jobs: || echo "WARNING: Gurobi is not available" python -m pip install --cache-dir cache/pip xpress \ || echo "WARNING: Xpress Community Edition is not available" + python -m pip install --cache-dir cache/pip pybnb \ + || echo "WARNING: pybnb is not available" if [[ ${{matrix.python}} == pypy* ]]; then echo "skipping wntr for pypy" else diff --git a/setup.py b/setup.py index cdb7e2f6c23..70c1626a650 100644 --- a/setup.py +++ b/setup.py @@ -285,7 +285,6 @@ def __ne__(self, other): 'sympy', # differentiation 'xlrd', # dataportals 'z3-solver', # community_detection - 'pybnb', # coramin # # subprocess output is merged more reliably if # 'PeekNamedPipe' is available from pywin32 From 706b7937720e5edff03d77598293f155207365ce Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Sat, 24 Feb 2024 22:54:22 -0700 Subject: [PATCH 108/128] fix imports --- pyomo/contrib/coramin/heuristics/diving.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyomo/contrib/coramin/heuristics/diving.py b/pyomo/contrib/coramin/heuristics/diving.py index 18511473eaf..74727adad4d 100644 --- a/pyomo/contrib/coramin/heuristics/diving.py +++ b/pyomo/contrib/coramin/heuristics/diving.py @@ -291,6 +291,7 @@ def run_diving_heuristic( integer_tol: float = 1e-4, time_limit: float = 300, node_limit: int = 1000, + comm=None, ): prob = DivingHeuristic(m) res: pybnb.SolverResults = pybnb.solve( @@ -299,6 +300,7 @@ def run_diving_heuristic( objective_stop=prob.infeasible_objective(), node_limit=node_limit, time_limit=time_limit, + comm=comm, ) ss = pybnb.SolutionStatus if res.solution_status in {ss.feasible, ss.optimal}: From e3330de51b82b6ffc0464a826c15248ff2716e2c Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Sat, 24 Feb 2024 23:09:31 -0700 Subject: [PATCH 109/128] fix tests --- .../coramin/algorithms/multitree/tests/test_multitree.py | 5 +++++ .../contrib/coramin/algorithms/tests/test_ecp_bounder.py | 4 ++-- pyomo/contrib/coramin/domain_reduction/tests/test_dbt.py | 9 +++++++++ .../contrib/coramin/heuristics/tests/test_heuristics.py | 4 ++++ pyomo/contrib/coramin/relaxations/tests/test_alphabb.py | 8 ++++++-- .../coramin/relaxations/tests/test_relaxations_base.py | 4 ++++ 6 files changed, 30 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/coramin/algorithms/multitree/tests/test_multitree.py b/pyomo/contrib/coramin/algorithms/multitree/tests/test_multitree.py index 181957769c1..a22596fdb7e 100644 --- a/pyomo/contrib/coramin/algorithms/multitree/tests/test_multitree.py +++ b/pyomo/contrib/coramin/algorithms/multitree/tests/test_multitree.py @@ -14,6 +14,10 @@ import shutil +ipopt_available = pe.SolverFactory('ipopt').available() +gurobi_available = appsi.solvers.Gurobi().available() + + def _get_sol(pname): start_x1_set = {'batch0812', 'chem'} current_dir = os.getcwd() @@ -59,6 +63,7 @@ def _check_relative_diff(self, expected, got, abs_tol=1e-3, rel_tol=1e-3): ) +@unittest.skipUnless(ipopt_available and gurobi_available, 'need both ipopt and gurobi') class TestMultiTreeWithMINLPLib(Helper): @classmethod def setUpClass(self) -> None: diff --git a/pyomo/contrib/coramin/algorithms/tests/test_ecp_bounder.py b/pyomo/contrib/coramin/algorithms/tests/test_ecp_bounder.py index a4b17c8d877..1a0ad410360 100644 --- a/pyomo/contrib/coramin/algorithms/tests/test_ecp_bounder.py +++ b/pyomo/contrib/coramin/algorithms/tests/test_ecp_bounder.py @@ -2,14 +2,14 @@ from pyomo.contrib import coramin from pyomo.contrib.coramin.algorithms.ecp_bounder import ECPBounder from pyomo.common import unittest -import logging from pyomo.contrib import appsi -logging.basicConfig(level=logging.INFO) +gurobi_available = appsi.solvers.Gurobi().available() class TestECPBounder(unittest.TestCase): + @unittest.skipUnless(gurobi_available, 'gurobi is not available') def test_ecp_bounder(self): m = pe.ConcreteModel() m.x = pe.Var() diff --git a/pyomo/contrib/coramin/domain_reduction/tests/test_dbt.py b/pyomo/contrib/coramin/domain_reduction/tests/test_dbt.py index ad04163329e..5e482036fd2 100644 --- a/pyomo/contrib/coramin/domain_reduction/tests/test_dbt.py +++ b/pyomo/contrib/coramin/domain_reduction/tests/test_dbt.py @@ -26,6 +26,15 @@ import pytest from pyomo.contrib.coramin.utils.compare_models import is_relaxation, is_equivalent from pyomo.contrib.coramin.utils.pyomo_utils import active_cons, active_vars +try: + import metis + metis_available = True +except ImportError: + metis_available = False + + +if not metis_available: + raise unittest.SkipTest('metis is not available') class TestDecomposition(unittest.TestCase): diff --git a/pyomo/contrib/coramin/heuristics/tests/test_heuristics.py b/pyomo/contrib/coramin/heuristics/tests/test_heuristics.py index 68f80c87a66..325d27de707 100644 --- a/pyomo/contrib/coramin/heuristics/tests/test_heuristics.py +++ b/pyomo/contrib/coramin/heuristics/tests/test_heuristics.py @@ -3,6 +3,10 @@ from pyomo.contrib.coramin.heuristics.diving import run_diving_heuristic +ipopt_available = pe.SolverFactory('ipopt').available() + + +@unittest.skipUnless(ipopt_available, 'ipopt is not available') class TestDiving(unittest.TestCase): def test_diving_1(self): m = pe.ConcreteModel() diff --git a/pyomo/contrib/coramin/relaxations/tests/test_alphabb.py b/pyomo/contrib/coramin/relaxations/tests/test_alphabb.py index 28b5c067866..10526157b3a 100644 --- a/pyomo/contrib/coramin/relaxations/tests/test_alphabb.py +++ b/pyomo/contrib/coramin/relaxations/tests/test_alphabb.py @@ -1,11 +1,13 @@ from pyomo.common import unittest -import itertools -import math import pyomo.environ as pe from pyomo.contrib import coramin from pyomo.contrib.coramin.relaxations.alphabb import AlphaBBRelaxation +ipopt_available = pe.SolverFactory('ipopt').available() +gurobi_available = pe.SolverFactory('appsi_gurobi').available() + + class TestAlphaBBRelaxation(unittest.TestCase): @classmethod def setUpClass(cls): @@ -26,6 +28,7 @@ def setUpClass(cls): eigenvalue_bounder=coramin.EigenValueBounder.GershgorinWithSimplification, ) + @unittest.skipUnless(ipopt_available, 'ipopt is not available') def test_nonlinear(self): model = self.model.clone() model.abb.use_linear_relaxation = False @@ -45,6 +48,7 @@ def test_nonlinear(self): solver.solve(model) self.assertLessEqual(model.w.value, pe.value(model.f_x)) + @unittest.skipUnless(gurobi_available, 'gurboi is not available') def test_linear(self): model = self.model.clone() model.abb.use_linear_relaxation = True diff --git a/pyomo/contrib/coramin/relaxations/tests/test_relaxations_base.py b/pyomo/contrib/coramin/relaxations/tests/test_relaxations_base.py index 994ec502055..4741df07f2d 100644 --- a/pyomo/contrib/coramin/relaxations/tests/test_relaxations_base.py +++ b/pyomo/contrib/coramin/relaxations/tests/test_relaxations_base.py @@ -5,6 +5,9 @@ from pyomo.core.base.var import SimpleVar +gurobi_available = pe.SolverFactory('appsi_gurobi').available() + + """ Things to test - relaxations are valid under all possible conditions @@ -29,6 +32,7 @@ class TestBaseRelaxation(unittest.TestCase): + @unittest.skipUnless(gurobi_available, 'gurboi is not available') def test_push_and_pop_oa_points(self): m = pe.ConcreteModel() m.x = pe.Var(bounds=(-2, 1)) From 81684d6cc9c9116d403ad55b97f59bf91efb6f95 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Sat, 24 Feb 2024 23:15:00 -0700 Subject: [PATCH 110/128] update GHA --- .github/workflows/test_branches.yml | 2 ++ .github/workflows/test_pr_and_main.yml | 2 ++ 2 files changed, 4 insertions(+) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 9f6af82a52e..01231ecaf42 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -361,6 +361,8 @@ jobs: CONDA_DEPENDENCIES="$CONDA_DEPENDENCIES $PKG" fi done + CONDA_DEPENDENCIES="$CONDA_DEPENDENCIES metis" + PYPI_DEPENDENCIES="$PYPI_DEPENDENCIES metis" echo "*** Install Pyomo dependencies ***" # Note: this will fail the build if any installation fails (or # possibly if it outputs messages to stderr) diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index 1ce5f673986..a42da98a397 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -383,6 +383,8 @@ jobs: CONDA_DEPENDENCIES="$CONDA_DEPENDENCIES $PKG" fi done + CONDA_DEPENDENCIES="$CONDA_DEPENDENCIES metis" + PYPI_DEPENDENCIES="$PYPI_DEPENDENCIES metis" echo "*** Install Pyomo dependencies ***" # Note: this will fail the build if any installation fails (or # possibly if it outputs messages to stderr) From 8d51683fe917818df65c46aaca5ab284618a1c2e Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Sat, 24 Feb 2024 23:22:52 -0700 Subject: [PATCH 111/128] fix available --- pyomo/contrib/appsi/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/appsi/base.py b/pyomo/contrib/appsi/base.py index 201e5975ac9..858d5859860 100644 --- a/pyomo/contrib/appsi/base.py +++ b/pyomo/contrib/appsi/base.py @@ -1641,7 +1641,7 @@ def solve( return legacy_results - def available(self, exception_flag=True): + def available(self, exception_flag=False): ans = super(LegacySolverInterface, self).available() if exception_flag and not ans: raise ApplicationError(f'Solver {self.__class__} is not available ({ans}).') From 87637f61a510ba2da8fe16585f1d3a08a1056a2a Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Sat, 24 Feb 2024 23:23:18 -0700 Subject: [PATCH 112/128] run black --- pyomo/contrib/coramin/domain_reduction/tests/test_dbt.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyomo/contrib/coramin/domain_reduction/tests/test_dbt.py b/pyomo/contrib/coramin/domain_reduction/tests/test_dbt.py index 5e482036fd2..753510fbec2 100644 --- a/pyomo/contrib/coramin/domain_reduction/tests/test_dbt.py +++ b/pyomo/contrib/coramin/domain_reduction/tests/test_dbt.py @@ -26,8 +26,10 @@ import pytest from pyomo.contrib.coramin.utils.compare_models import is_relaxation, is_equivalent from pyomo.contrib.coramin.utils.pyomo_utils import active_cons, active_vars + try: import metis + metis_available = True except ImportError: metis_available = False From 9f6afd9b6b2c724cf4a740af0c24e940eea48c38 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 4 Mar 2024 07:53:25 -0700 Subject: [PATCH 113/128] osil parser --- .../coramin/third_party/minlplib_tools.py | 312 +++++++++++++++++- 1 file changed, 309 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/coramin/third_party/minlplib_tools.py b/pyomo/contrib/coramin/third_party/minlplib_tools.py index e167d9f340a..38d722bf101 100644 --- a/pyomo/contrib/coramin/third_party/minlplib_tools.py +++ b/pyomo/contrib/coramin/third_party/minlplib_tools.py @@ -1,12 +1,15 @@ import os -from zipfile import ZipFile -from pyomo.common import fileutils from pyomo.common import download -import enum import math import csv from collections.abc import Iterable import logging +from xml.etree import ElementTree +import pyomo.environ as pe +from pyomo.core.base.block import ScalarBlock +from pyomo.core.base.var import IndexedVar +from pyomo.core.base.constraint import IndexedConstraint +from pyomo.core.expr.numeric_expr import LinearExpression logger = logging.getLogger(__name__) @@ -416,3 +419,306 @@ def get_minlplib(download_dir=None, format='osil', problem_name=None): downloader.get_binary_file( 'http://www.minlplib.org/' + format + '/' + problem_name + '.' + format ) + + +def _handle_negate_osil(node, var_map): + assert len(node) == 1 + return -_parse_nonlinear_expression_osil(node[0], var_map) + + +def _handle_divide_osil(node, var_map): + assert len(node) == 2 + arg1 = _parse_nonlinear_expression_osil(node[0], var_map) + arg2 = _parse_nonlinear_expression_osil(node[1], var_map) + return arg1 / arg2 + + +def _handle_sum_osil(node, var_map): + res = 0 + for i in node: + arg = _parse_nonlinear_expression_osil(i, var_map) + res += arg + return res + + +def _handle_product_osil(node, var_map): + res = 1 + for i in node: + arg = _parse_nonlinear_expression_osil(i, var_map) + res *= arg + return res + + +def _handle_variable_osil(node, var_map): + assert len(node) == 0 + ndx = int(node.attrib['idx']) + v = var_map[ndx] + if 'coef' in node.attrib: + coef = float(node.attrib['coef']) + else: + coef = 1 + return v * coef + + +def _handle_log_osil(node, var_map): + assert len(node) == 1 + return pe.log(_parse_nonlinear_expression_osil(node[0], var_map)) + + +def _handle_exp_osil(node, var_map): + assert len(node) == 1 + return pe.exp(_parse_nonlinear_expression_osil(node[0], var_map)) + + +def _handle_number_osil(node, var_map): + assert len(node) == 0 + return float(node.attrib['value']) + + +def _handle_square_osil(node, var_map): + assert len(node) == 1 + return _parse_nonlinear_expression_osil(node[0], var_map) ** 2 + + +def _handle_power_osil(node, var_map): + assert len(node) == 2 + arg1 = _parse_nonlinear_expression_osil(node[0], var_map) + arg2 = _parse_nonlinear_expression_osil(node[1], var_map) + return arg1**arg2 + + +_osil_operator_map = dict() +_osil_operator_map['{os.optimizationservices.org}negate'] = _handle_negate_osil +_osil_operator_map['{os.optimizationservices.org}divide'] = _handle_divide_osil +_osil_operator_map['{os.optimizationservices.org}sum'] = _handle_sum_osil +_osil_operator_map['{os.optimizationservices.org}product'] = _handle_product_osil +_osil_operator_map['{os.optimizationservices.org}variable'] = _handle_variable_osil +_osil_operator_map['{os.optimizationservices.org}ln'] = _handle_log_osil +_osil_operator_map['{os.optimizationservices.org}exp'] = _handle_exp_osil +_osil_operator_map['{os.optimizationservices.org}number'] = _handle_number_osil +_osil_operator_map['{os.optimizationservices.org}square'] = _handle_square_osil +_osil_operator_map['{os.optimizationservices.org}power'] = _handle_power_osil + + +def _parse_nonlinear_expression_osil(node, var_map): + return _osil_operator_map[node.tag](node, var_map) + + +def parse_osil_file(fname) -> ScalarBlock: + tree = ElementTree.parse(fname) + ns = '{os.optimizationservices.org}' + root = tree.getroot() + + instance_data = list(root.iter(ns + 'instanceData')) + assert len(instance_data) == 1 + instance_data = instance_data[0] + acceptable_nodes = set( + ns + i + for i in [ + 'variables', + 'objectives', + 'constraints', + 'linearConstraintCoefficients', + 'quadraticCoefficients', + 'nonlinearExpressions', + ] + ) + for i in instance_data: + if i.tag not in acceptable_nodes: + raise ValueError(f'Unexpected xml node: {i.tag}') + instance_data_nodes = set(i.tag for i in instance_data) + + m = ScalarBlock(concrete=True) + + variables_node = list(instance_data.iter(ns + 'variables'))[0] + vnames = list() + for v in variables_node.iter(ns + 'var'): + vnames.append(v.attrib['name']) + + m.var_names = pe.Set(initialize=vnames) + m.vars = IndexedVar(m.var_names) + + type_map = {'B': pe.Binary} + + for v in variables_node.iter(ns + 'var'): + vdata = v.attrib + vname = vdata.pop('name') + if 'lb' in vdata: + vlb = vdata.pop('lb') + if vlb == '-INF': + vlb = None + else: + vlb = float(vlb) + else: + vlb = 0 + if 'ub' in vdata: + vub = float(vdata.pop('ub')) + else: + vub = None + if 'type' in vdata: + if vdata['type'] not in type_map: + raise ValueError(f"Unrecognized variable type: {vdata['type']}") + vtype = type_map[vdata.pop('type')] + else: + vtype = pe.Reals + m.vars[vname].setlb(vlb) + m.vars[vname].setub(vub) + m.vars[vname].domain = vtype + assert len(vdata) == 0 + + con_names = [] + con_lbs = [] + con_ubs = [] + constraints_node = list(instance_data.iter(ns + 'constraints'))[0] + for c in constraints_node.iter(ns + 'con'): + cdata = c.attrib + cname = cdata.pop('name') + if 'lb' in cdata: + clb = float(cdata.pop('lb')) + else: + clb = None + if 'ub' in cdata: + cub = float(cdata.pop('ub')) + else: + cub = None + con_names.append(cname) + con_lbs.append(clb) + con_ubs.append(cub) + + # osil format specifies the linear parts of the constraints in CSR format + if (ns + 'linearConstraintCoefficients') in instance_data_nodes: + linpart = list(instance_data.iter(ns + 'linearConstraintCoefficients')) + assert len(linpart) == 1 + linpart = linpart[0] + rowstart = list(linpart.iter(ns + 'start'))[0] + colind = list(linpart.iter(ns + 'colIdx'))[0] + vals = list(linpart.iter(ns + 'value'))[0] + + tmp = list() + for i in rowstart: + s = int(i.text) + n = int(i.attrib.pop('mult', 1)) - 1 + step = int(i.attrib.pop('incr', 0)) + tmp.append(s) + for _ in range(n): + s += step + tmp.append(s) + rowstart = tmp + assert len(rowstart) == len(con_names) + 1 + + tmp = list() + for i in colind: + s = int(i.text) + n = int(i.attrib.pop('mult', 1)) - 1 + step = int(i.attrib.pop('incr', 0)) + tmp.append(s) + for _ in range(n): + s += step + tmp.append(s) + colind = tmp + + tmp = list() + for i in vals: + s = float(i.text) + n = int(i.attrib.pop('mult', 1)) + for _ in range(n): + tmp.append(s) + vals = tmp + + linear_parts = list() + for row in range(len(con_names)): + if rowstart[row] == rowstart[row + 1]: + linear_parts.append(0) + else: + coefs = vals[rowstart[row] : rowstart[row + 1]] + var_indices = colind[rowstart[row] : rowstart[row + 1]] + _vars = [m.vars[vnames[i]] for i in var_indices] + linear_parts.append( + LinearExpression(constant=0, linear_coefs=coefs, linear_vars=_vars) + ) + else: + linear_parts = [0] * len(con_names) + + quad_exprs = [0] * len(con_names) + obj_expr = 0 + if (ns + 'quadraticCoefficients') in instance_data_nodes: + quadpart = list(instance_data.iter(ns + 'quadraticCoefficients')) + assert len(quadpart) == 1 + quadpart = quadpart[0] + for i in quadpart: + row_ndx = int(i.attrib['idx']) + col1 = int(i.attrib['idxOne']) + col2 = int(i.attrib['idxTwo']) + v1 = m.vars[vnames[col1]] + v2 = m.vars[vnames[col2]] + coef = float(i.attrib['coef']) + if row_ndx == -1: + obj_expr += coef * (v1 * v2) + else: + quad_exprs[row_ndx] += coef * (v1 * v2) + + var_map = dict() + for var_ndx, var_name in enumerate(vnames): + var_map[var_ndx] = m.vars[var_name] + nl_exprs = [0] * len(con_names) + if (ns + 'nonlinearExpressions') in instance_data_nodes: + nlpart = list(instance_data.iter(ns + 'nonlinearExpressions')) + assert len(nlpart) == 1 + nlpart = nlpart[0] + for i in nlpart: + row_ndx = int(i.attrib['idx']) + assert len(i) == 1 + expr = _parse_nonlinear_expression_osil(i[0], var_map) + if row_ndx == -1: + obj_expr += expr + else: + nl_exprs[row_ndx] = expr + + m.con_names = pe.Set(initialize=con_names) + m.cons = IndexedConstraint(m.con_names) + for ndx, cname in enumerate(con_names): + l = linear_parts[ndx] + q = quad_exprs[ndx] + n = nl_exprs[ndx] + lb = con_lbs[ndx] + ub = con_ubs[ndx] + if lb == ub and lb is not None: + m.cons[cname] = l + q + n == lb + else: + m.cons[cname] = (lb, l + q + n, ub) + + if (ns + 'objectives') in instance_data_nodes: + obj_node = list(instance_data.iter(ns + 'objectives')) + assert len(obj_node) == 1 + obj_node = obj_node[0] + # yes - this really does need repeated + assert len(obj_node) == 1 + obj_node = obj_node[0] + sense_str = obj_node.attrib['maxOrMin'] + if sense_str == 'min': + sense = pe.minimize + else: + assert sense_str == 'max' + sense = pe.maximize + + lin_coefs = list() + lin_vars = list() + obj_const = float(obj_node.attrib.pop('constant', 0)) + for node in obj_node: + var_ndx = int(node.attrib['idx']) + var_name = vnames[var_ndx] + coef = float(node.text) + lin_coefs.append(coef) + lin_vars.append(m.vars[var_name]) + if len(lin_coefs) > 0: + obj_expr += LinearExpression( + constant=obj_const, linear_coefs=lin_coefs, linear_vars=lin_vars + ) + else: + obj_expr += obj_const + else: + sense = pe.minimize + + m.objective = pe.Objective(expr=obj_expr, sense=sense) + + return m From 9f7a1f0c27cd53165c062d2af2f6f2ede7574cb9 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 4 Mar 2024 08:00:54 -0700 Subject: [PATCH 114/128] use osil parser in tests --- .../coramin/algorithms/bnb/tests/test_bnb.py | 12 +++++------- .../algorithms/multitree/tests/test_multitree.py | 14 ++++++-------- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/pyomo/contrib/coramin/algorithms/bnb/tests/test_bnb.py b/pyomo/contrib/coramin/algorithms/bnb/tests/test_bnb.py index f193ecb44a9..bba875da118 100644 --- a/pyomo/contrib/coramin/algorithms/bnb/tests/test_bnb.py +++ b/pyomo/contrib/coramin/algorithms/bnb/tests/test_bnb.py @@ -1,7 +1,7 @@ import math from pyomo.contrib import coramin -from pyomo.contrib.coramin.third_party.minlplib_tools import get_minlplib +from pyomo.contrib.coramin.third_party.minlplib_tools import get_minlplib, parse_osil_file from pyomo.common import unittest from pyomo.contrib import appsi import os @@ -10,7 +10,6 @@ from pyomo.common import download import pyomo.environ as pe from pyomo.core.base.block import _BlockData -import importlib import shutil @@ -88,17 +87,16 @@ def setUpClass(self) -> None: def tearDownClass(self) -> None: current_dir = os.getcwd() for pname in self.test_problems.keys(): - os.remove(os.path.join(current_dir, 'minlplib', 'py', f'{pname}.py')) - shutil.rmtree(os.path.join(current_dir, 'minlplib', 'py')) + os.remove(os.path.join(current_dir, 'minlplib', 'osil', f'{pname}.osil')) + shutil.rmtree(os.path.join(current_dir, 'minlplib', 'osil')) os.rmdir(os.path.join(current_dir, 'minlplib')) for pname in self.primal_sol.keys(): os.remove(os.path.join(current_dir, f'{pname}.sol')) def get_model(self, pname): current_dir = os.getcwd() - fname = os.path.join('minlplib', 'py', f'{pname}') - fname = fname.replace('/', '.') - m = importlib.import_module(fname).m + fname = os.path.join(current_dir, 'minlplib', 'osil', f'{pname}.osil') + m = parse_osil_file(fname) return m def _check_primal_sol(self, pname, m: _BlockData, res: appsi.base.Results): diff --git a/pyomo/contrib/coramin/algorithms/multitree/tests/test_multitree.py b/pyomo/contrib/coramin/algorithms/multitree/tests/test_multitree.py index a22596fdb7e..2bcf7e74ea8 100644 --- a/pyomo/contrib/coramin/algorithms/multitree/tests/test_multitree.py +++ b/pyomo/contrib/coramin/algorithms/multitree/tests/test_multitree.py @@ -1,7 +1,7 @@ import math from pyomo.contrib import coramin -from pyomo.contrib.coramin.third_party.minlplib_tools import get_minlplib +from pyomo.contrib.coramin.third_party.minlplib_tools import get_minlplib, parse_osil_file from pyomo.common import unittest from pyomo.contrib import appsi import os @@ -10,7 +10,6 @@ from pyomo.common import download import pyomo.environ as pe from pyomo.core.base.block import _BlockData -import importlib import shutil @@ -84,7 +83,7 @@ def setUpClass(self) -> None: self.primal_sol['autocorr_bern20-03'] = _get_sol('autocorr_bern20-03') self.primal_sol['chem'] = _get_sol('chem') for pname in self.test_problems.keys(): - get_minlplib(problem_name=pname, format='py') + get_minlplib(problem_name=pname, format='osil') mip_solver = appsi.solvers.Gurobi() nlp_solver = appsi.solvers.Ipopt() nlp_solver.config.log_level = logging.DEBUG @@ -96,17 +95,16 @@ def setUpClass(self) -> None: def tearDownClass(self) -> None: current_dir = os.getcwd() for pname in self.test_problems.keys(): - os.remove(os.path.join(current_dir, 'minlplib', 'py', f'{pname}.py')) - shutil.rmtree(os.path.join(current_dir, 'minlplib', 'py')) + os.remove(os.path.join(current_dir, 'minlplib', 'osil', f'{pname}.osil')) + shutil.rmtree(os.path.join(current_dir, 'minlplib', 'osil')) os.rmdir(os.path.join(current_dir, 'minlplib')) for pname in self.primal_sol.keys(): os.remove(os.path.join(current_dir, f'{pname}.sol')) def get_model(self, pname): current_dir = os.getcwd() - fname = os.path.join('minlplib', 'py', f'{pname}') - fname = fname.replace('/', '.') - m = importlib.import_module(fname).m + fname = os.path.join(current_dir, 'minlplib', 'osil', f'{pname}.osil') + m = parse_osil_file(fname) return m def _check_primal_sol(self, pname, m: _BlockData, res: appsi.base.Results): From e0e9f347d077fef836f86b75cbea992eea7c2e01 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 4 Mar 2024 08:36:20 -0700 Subject: [PATCH 115/128] bugs --- .../coramin/algorithms/bnb/tests/test_bnb.py | 14 ++------------ .../coramin/algorithms/multitree/multitree.py | 4 ++++ .../algorithms/multitree/tests/test_multitree.py | 12 +----------- .../contrib/coramin/third_party/minlplib_tools.py | 2 +- 4 files changed, 8 insertions(+), 24 deletions(-) diff --git a/pyomo/contrib/coramin/algorithms/bnb/tests/test_bnb.py b/pyomo/contrib/coramin/algorithms/bnb/tests/test_bnb.py index bba875da118..bb4b348ac12 100644 --- a/pyomo/contrib/coramin/algorithms/bnb/tests/test_bnb.py +++ b/pyomo/contrib/coramin/algorithms/bnb/tests/test_bnb.py @@ -29,16 +29,6 @@ def _get_sol(pname): if vname == 'objvar': continue assert vname.startswith('x') or vname.startswith('b') - if vname.startswith('x'): - ndx = int(vname.replace('x', '')) - 1 - if pname in start_x1_set: - ndx += 1 - vname = f'x{ndx}' - else: - ndx = int(vname.replace('b', '')) - 1 - if pname in start_x1_set: - ndx += 1 - vname = f'b{ndx}' res[vname] = vval f.close() return res @@ -78,7 +68,7 @@ def setUpClass(self) -> None: self.primal_sol['autocorr_bern20-03'] = _get_sol('autocorr_bern20-03') self.primal_sol['chem'] = _get_sol('chem') for pname in self.test_problems.keys(): - get_minlplib(problem_name=pname, format='py') + get_minlplib(problem_name=pname, format='osil') self.opt = coramin.algorithms.BnBSolver() self.opt.config.lp_solver = appsi.solvers.Highs() self.opt.config.nlp_solver = pe.SolverFactory('ipopt') @@ -103,7 +93,7 @@ def _check_primal_sol(self, pname, m: _BlockData, res: appsi.base.Results): expected_by_str = self.primal_sol[pname] expected_by_var = pe.ComponentMap() for vname, vval in expected_by_str.items(): - v = getattr(m, vname) + v = m.vars[vname] expected_by_var[v] = vval got = res.solution_loader.get_primals() for v, val in expected_by_var.items(): diff --git a/pyomo/contrib/coramin/algorithms/multitree/multitree.py b/pyomo/contrib/coramin/algorithms/multitree/multitree.py index e605d85d14b..a1221a7d221 100644 --- a/pyomo/contrib/coramin/algorithms/multitree/multitree.py +++ b/pyomo/contrib/coramin/algorithms/multitree/multitree.py @@ -907,6 +907,10 @@ def solve( model, self._orig_var_map = get_clone_and_var_map(model) + # prevent the model from being garbage collected; + # otherwise the variables will become "unattached" + self.truely_original_model = model + self._original_model, self._relaxation = clone_shallow_active_flat(model, 2) model = self._original_model diff --git a/pyomo/contrib/coramin/algorithms/multitree/tests/test_multitree.py b/pyomo/contrib/coramin/algorithms/multitree/tests/test_multitree.py index 2bcf7e74ea8..415db52b3fe 100644 --- a/pyomo/contrib/coramin/algorithms/multitree/tests/test_multitree.py +++ b/pyomo/contrib/coramin/algorithms/multitree/tests/test_multitree.py @@ -33,16 +33,6 @@ def _get_sol(pname): if vname == 'objvar': continue assert vname.startswith('x') or vname.startswith('b') - if vname.startswith('x'): - ndx = int(vname.replace('x', '')) - 1 - if pname in start_x1_set: - ndx += 1 - vname = f'x{ndx}' - else: - ndx = int(vname.replace('b', '')) - 1 - if pname in start_x1_set: - ndx += 1 - vname = f'b{ndx}' res[vname] = vval f.close() return res @@ -111,7 +101,7 @@ def _check_primal_sol(self, pname, m: _BlockData, res: appsi.base.Results): expected_by_str = self.primal_sol[pname] expected_by_var = pe.ComponentMap() for vname, vval in expected_by_str.items(): - v = getattr(m, vname) + v = m.vars[vname] expected_by_var[v] = vval got = res.solution_loader.get_primals() for v, val in expected_by_var.items(): diff --git a/pyomo/contrib/coramin/third_party/minlplib_tools.py b/pyomo/contrib/coramin/third_party/minlplib_tools.py index 38d722bf101..ff37cf12a51 100644 --- a/pyomo/contrib/coramin/third_party/minlplib_tools.py +++ b/pyomo/contrib/coramin/third_party/minlplib_tools.py @@ -538,7 +538,7 @@ def parse_osil_file(fname) -> ScalarBlock: m.var_names = pe.Set(initialize=vnames) m.vars = IndexedVar(m.var_names) - type_map = {'B': pe.Binary} + type_map = {'B': pe.Binary, 'I': pe.Integers} for v in variables_node.iter(ns + 'var'): vdata = v.attrib From b36459b37d18691ea7d5cfdefebae493ea1d0714 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 4 Mar 2024 10:01:34 -0700 Subject: [PATCH 116/128] update tests --- .../coramin/algorithms/bnb/tests/test_bnb.py | 8 +++- .../multitree/tests/test_multitree.py | 1 + .../domain_reduction/tests/test_filters.py | 4 ++ .../relaxations/tests/test_auto_relax.py | 4 ++ .../relaxations/tests/test_mccormick.py | 5 +++ .../relaxations/tests/test_relaxations.py | 4 ++ .../tests/test_univariate_relaxations.py | 6 +++ .../third_party/tests/test_minlplib_tools.py | 42 +++++++++++-------- .../utils/tests/test_compare_models.py | 4 ++ 9 files changed, 60 insertions(+), 18 deletions(-) diff --git a/pyomo/contrib/coramin/algorithms/bnb/tests/test_bnb.py b/pyomo/contrib/coramin/algorithms/bnb/tests/test_bnb.py index bb4b348ac12..5b2a3485077 100644 --- a/pyomo/contrib/coramin/algorithms/bnb/tests/test_bnb.py +++ b/pyomo/contrib/coramin/algorithms/bnb/tests/test_bnb.py @@ -13,6 +13,10 @@ import shutil +highs_available = appsi.solvers.Highs().available() +ipopt_available = pe.SolverFactory('ipopt').available() + + def _get_sol(pname): start_x1_set = {'batch0812', 'chem'} current_dir = os.getcwd() @@ -48,6 +52,7 @@ def _check_relative_diff(self, expected, got, abs_tol=1e-3, rel_tol=1e-3): ) +@unittest.skipUnless(ipopt_available and highs_available, 'need both ipopt and highs') class TestBnBWithMINLPLib(Helper): @classmethod def setUpClass(self) -> None: @@ -174,7 +179,8 @@ def test_available(self): assert avail in appsi.base.Solver.Availability -class TestMultiTree(Helper): +@unittest.skipUnless(ipopt_available and highs_available, 'need both ipopt and highs') +class TestBnB(Helper): def test_convex_overestimator(self): m = pe.ConcreteModel() m.x = pe.Var(bounds=(-2, 1)) diff --git a/pyomo/contrib/coramin/algorithms/multitree/tests/test_multitree.py b/pyomo/contrib/coramin/algorithms/multitree/tests/test_multitree.py index 415db52b3fe..d16d14af027 100644 --- a/pyomo/contrib/coramin/algorithms/multitree/tests/test_multitree.py +++ b/pyomo/contrib/coramin/algorithms/multitree/tests/test_multitree.py @@ -185,6 +185,7 @@ def test_available(self): assert avail in appsi.base.Solver.Availability +@unittest.skipUnless(ipopt_available and gurobi_available, 'need both ipopt and gurobi') class TestMultiTree(Helper): def test_convex_overestimator(self): m = pe.ConcreteModel() diff --git a/pyomo/contrib/coramin/domain_reduction/tests/test_filters.py b/pyomo/contrib/coramin/domain_reduction/tests/test_filters.py index 73a32461aff..8e614e3a3da 100644 --- a/pyomo/contrib/coramin/domain_reduction/tests/test_filters.py +++ b/pyomo/contrib/coramin/domain_reduction/tests/test_filters.py @@ -4,6 +4,10 @@ from pyomo.contrib import appsi +highs_available = appsi.solvers.Highs().available() + + +@unittest.skipUnless(highs_available, 'HiGHS is not available') class TestFilters(unittest.TestCase): def test_basic_filter(self): m = pe.ConcreteModel() diff --git a/pyomo/contrib/coramin/relaxations/tests/test_auto_relax.py b/pyomo/contrib/coramin/relaxations/tests/test_auto_relax.py index 5ab3918f277..17bbd085b2b 100644 --- a/pyomo/contrib/coramin/relaxations/tests/test_auto_relax.py +++ b/pyomo/contrib/coramin/relaxations/tests/test_auto_relax.py @@ -13,6 +13,9 @@ from pyomo.core.expr.compare import assertExpressionsEqual +gurobi_available = appsi.solvers.Gurobi().available() + + class TestAutoRelax(unittest.TestCase): def test_product1(self): m = pe.ConcreteModel() @@ -965,6 +968,7 @@ def _pow_neg_1point2(x): return x ** (-1.2) +@unittest.skipUnless(gurobi_available, 'Gurobi is not available') class TestUnivariate(unittest.TestCase): def helper(self, func, bounds_list): for relaxation_side in [ diff --git a/pyomo/contrib/coramin/relaxations/tests/test_mccormick.py b/pyomo/contrib/coramin/relaxations/tests/test_mccormick.py index 4cfafa5e832..7e927b50b6f 100644 --- a/pyomo/contrib/coramin/relaxations/tests/test_mccormick.py +++ b/pyomo/contrib/coramin/relaxations/tests/test_mccormick.py @@ -1,8 +1,13 @@ import pyomo.environ as pyo from pyomo.common import unittest from pyomo.contrib import coramin +from pyomo.contrib import appsi +gurobi_available = pyo.SolverFactory('gurobi_direct').available() + + +@unittest.skipUnless(gurobi_available, 'Gurobi is not available') class TestMcCormick(unittest.TestCase): @classmethod def setUpClass(cls): diff --git a/pyomo/contrib/coramin/relaxations/tests/test_relaxations.py b/pyomo/contrib/coramin/relaxations/tests/test_relaxations.py index 423498b12dd..f3d38aded82 100644 --- a/pyomo/contrib/coramin/relaxations/tests/test_relaxations.py +++ b/pyomo/contrib/coramin/relaxations/tests/test_relaxations.py @@ -19,6 +19,9 @@ import io +gurobi_available = appsi.solvers.Gurobi().available() + + def _grid_rhs_vars( v_list: Sequence[_GeneralVarData], num_points: int = 30 ) -> List[Tuple[float, ...]]: @@ -198,6 +201,7 @@ def _check_scaling(m: _BlockData, rel: coramin.relaxations.BaseRelaxationData) - return passed +@unittest.skipUnless(gurobi_available, 'Gurobi is not available') class TestRelaxationBasics(unittest.TestCase): def valid_relaxation_helper( self, diff --git a/pyomo/contrib/coramin/relaxations/tests/test_univariate_relaxations.py b/pyomo/contrib/coramin/relaxations/tests/test_univariate_relaxations.py index b2ed7875fb5..5ddf85220dc 100644 --- a/pyomo/contrib/coramin/relaxations/tests/test_univariate_relaxations.py +++ b/pyomo/contrib/coramin/relaxations/tests/test_univariate_relaxations.py @@ -6,6 +6,10 @@ from pyomo.contrib.coramin.relaxations.segments import compute_k_segment_points +gurobi_available = pe.SolverFactory('gurobi_direct').available() + + +@unittest.skipUnless(gurobi_available, 'Gurobi is not available') class TestUnivariateExp(unittest.TestCase): @classmethod def setUpClass(cls): @@ -56,6 +60,7 @@ def test_exp_lb(self): self.assertAlmostEqual(pe.value(model.y), math.exp(-1.5), 4) +@unittest.skipUnless(gurobi_available, 'Gurobi is not available') class TestUnivariate(unittest.TestCase): def helper( self, @@ -206,6 +211,7 @@ def test_cos(self): ) +@unittest.skipUnless(gurobi_available, 'Gurobi is not available') class TestFeasibility(unittest.TestCase): def test_univariate_exp(self): m = pe.ConcreteModel() diff --git a/pyomo/contrib/coramin/third_party/tests/test_minlplib_tools.py b/pyomo/contrib/coramin/third_party/tests/test_minlplib_tools.py index 7eac973f830..4427f6ffc36 100644 --- a/pyomo/contrib/coramin/third_party/tests/test_minlplib_tools.py +++ b/pyomo/contrib/coramin/third_party/tests/test_minlplib_tools.py @@ -4,7 +4,7 @@ from pyomo.common.fileutils import this_file_dir from urllib.request import urlopen from socket import timeout - +import shutil try: urlopen('http://www.minlplib.org', timeout=3) @@ -13,14 +13,22 @@ class TestMINLPLibTools(unittest.TestCase): + @classmethod + def tearDownClass(self) -> None: + current_dir = this_file_dir() + print(current_dir) + if os.path.exists(os.path.join(current_dir, 'minlplib')): + shutil.rmtree(os.path.join(current_dir, 'minlplib')) + def test_get_minlplib_instancedata(self): - current_dir = os.getcwd() - coramin.third_party.get_minlplib_instancedata() + current_dir = this_file_dir() + fname = os.path.join(current_dir, 'minlplib', 'instancedata.csv') + coramin.third_party.get_minlplib_instancedata(target_filename=fname) self.assertTrue( os.path.exists(os.path.join(current_dir, 'minlplib', 'instancedata.csv')) ) - cases = coramin.third_party.filter_minlplib_instances() - self.assertEqual(len(cases), 1595) + cases = coramin.third_party.filter_minlplib_instances(instancedata_filename=fname) + self.assertEqual(len(cases), 1595 + 7) os.remove(os.path.join(current_dir, 'minlplib', 'instancedata.csv')) os.rmdir(os.path.join(current_dir, 'minlplib')) @@ -30,7 +38,7 @@ def test_filter_minlplib_instances(self): target_filename=os.path.join(current_dir, 'minlplib', 'instancedata.csv') ) - total_cases = 1595 + total_cases = 1595 + 7 cases = coramin.third_party.filter_minlplib_instances( instancedata_filename=os.path.join( current_dir, 'minlplib', 'instancedata.csv' @@ -63,7 +71,7 @@ def test_filter_minlplib_instances(self): ), acceptable_probtype=['QCQP', 'MIQCQP', 'MBQCQP'], ) - self.assertEqual(len(cases), 56) # regression + self.assertEqual(len(cases), 56 + 2) # regression cases = coramin.third_party.filter_minlplib_instances( instancedata_filename=os.path.join( @@ -91,7 +99,7 @@ def test_filter_minlplib_instances(self): min_nvars=2, max_nvars=200000, ) - self.assertEqual(len(cases), total_cases - 16 - 1) # unit + self.assertEqual(len(cases), total_cases - 16 - 1 - 2) # unit cases = coramin.third_party.filter_minlplib_instances( instancedata_filename=os.path.join( @@ -115,7 +123,7 @@ def test_filter_minlplib_instances(self): ), max_ncons=164000, ) - self.assertEqual(len(cases), total_cases - 1) # unit + self.assertEqual(len(cases), total_cases - 4) # unit cases = coramin.third_party.filter_minlplib_instances( instancedata_filename=os.path.join( @@ -140,7 +148,7 @@ def test_filter_minlplib_instances(self): ), max_nnlvars=199998, ) - self.assertEqual(len(cases), total_cases - 1) # unit + self.assertEqual(len(cases), total_cases - 2) # unit cases = coramin.third_party.filter_minlplib_instances( instancedata_filename=os.path.join( @@ -180,7 +188,7 @@ def test_filter_minlplib_instances(self): ), max_nlincons=164319, ) - self.assertEqual(len(cases), total_cases - 1) # unit + self.assertEqual(len(cases), total_cases - 3) # unit cases = coramin.third_party.filter_minlplib_instances( instancedata_filename=os.path.join( @@ -188,7 +196,7 @@ def test_filter_minlplib_instances(self): ), max_nquadcons=139999, ) - self.assertEqual(len(cases), total_cases - 1) # unit + self.assertEqual(len(cases), total_cases - 2) # unit cases = coramin.third_party.filter_minlplib_instances( instancedata_filename=os.path.join( @@ -236,7 +244,7 @@ def test_filter_minlplib_instances(self): ), max_nlaghessiandiagnz=100000, ) - self.assertEqual(len(cases), total_cases - 1) # unit + self.assertEqual(len(cases), total_cases - 3) # unit cases = coramin.third_party.filter_minlplib_instances( instancedata_filename=os.path.join( @@ -252,7 +260,7 @@ def test_filter_minlplib_instances(self): ), acceptable_objcurvature=['linear', 'convex'], ) - self.assertEqual(len(cases), 1220) # unit + self.assertEqual(len(cases), 1220 + 7) # unit os.remove(os.path.join(current_dir, 'minlplib', 'instancedata.csv')) os.rmdir(os.path.join(current_dir, 'minlplib')) @@ -263,7 +271,7 @@ def test_get_minlplib(self): download_dir=os.path.join(current_dir, 'minlplib', 'osil') ) files = os.listdir(os.path.join(current_dir, 'minlplib', 'osil')) - self.assertEqual(len(files), 1594) + self.assertEqual(len(files), 1594 + 7) for i in files: self.assertTrue(i.endswith('.osil')) for i in os.listdir(os.path.join(current_dir, 'minlplib', 'osil')): @@ -272,8 +280,8 @@ def test_get_minlplib(self): os.rmdir(os.path.join(current_dir, 'minlplib')) def test_get_minlplib_problem(self): - current_dir = os.getcwd() - coramin.third_party.get_minlplib(format='gms', problem_name='ex4_1_1') + current_dir = this_file_dir() + coramin.third_party.get_minlplib(download_dir=os.path.join(current_dir, 'minlplib', 'gms'), format='gms', problem_name='ex4_1_1') files = os.listdir(os.path.join(current_dir, 'minlplib', 'gms')) self.assertEqual(len(files), 1) self.assertEqual(files[0], 'ex4_1_1.gms') diff --git a/pyomo/contrib/coramin/utils/tests/test_compare_models.py b/pyomo/contrib/coramin/utils/tests/test_compare_models.py index 669c98d9bb8..be3fadc078a 100644 --- a/pyomo/contrib/coramin/utils/tests/test_compare_models.py +++ b/pyomo/contrib/coramin/utils/tests/test_compare_models.py @@ -9,7 +9,11 @@ from pyomo.core.expr.compare import assertExpressionsEqual, compare_expressions +highs_available = appsi.solvers.Highs().available() + + class TestCompareModels(unittest.TestCase): + @unittest.skipUnless(highs_available, 'HiGHS is not available') def test_compare_models_1(self): m1 = pe.ConcreteModel() m1.x = x = pe.Var(bounds=(-5, 4)) From 7e7364a8e6c487ccff1d917c1025198a5f1267f1 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 4 Mar 2024 10:30:28 -0700 Subject: [PATCH 117/128] update tests --- pyomo/contrib/coramin/relaxations/tests/test_mccormick.py | 2 +- .../coramin/relaxations/tests/test_univariate_relaxations.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/coramin/relaxations/tests/test_mccormick.py b/pyomo/contrib/coramin/relaxations/tests/test_mccormick.py index 7e927b50b6f..89d213c8f49 100644 --- a/pyomo/contrib/coramin/relaxations/tests/test_mccormick.py +++ b/pyomo/contrib/coramin/relaxations/tests/test_mccormick.py @@ -4,7 +4,7 @@ from pyomo.contrib import appsi -gurobi_available = pyo.SolverFactory('gurobi_direct').available() +gurobi_available = appsi.solvers.Gurobi().available() @unittest.skipUnless(gurobi_available, 'Gurobi is not available') diff --git a/pyomo/contrib/coramin/relaxations/tests/test_univariate_relaxations.py b/pyomo/contrib/coramin/relaxations/tests/test_univariate_relaxations.py index 5ddf85220dc..417b4b470cc 100644 --- a/pyomo/contrib/coramin/relaxations/tests/test_univariate_relaxations.py +++ b/pyomo/contrib/coramin/relaxations/tests/test_univariate_relaxations.py @@ -4,9 +4,10 @@ from pyomo.contrib import coramin import numpy as np from pyomo.contrib.coramin.relaxations.segments import compute_k_segment_points +from pyomo.contrib import appsi -gurobi_available = pe.SolverFactory('gurobi_direct').available() +gurobi_available = appsi.solvers.Gurobi().available() @unittest.skipUnless(gurobi_available, 'Gurobi is not available') From 1c3fe3a780ac9e22d63dc6a0d858bd3fcee51b30 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 4 Mar 2024 10:34:30 -0700 Subject: [PATCH 118/128] update tests --- pyomo/contrib/coramin/domain_reduction/tests/test_obbt.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyomo/contrib/coramin/domain_reduction/tests/test_obbt.py b/pyomo/contrib/coramin/domain_reduction/tests/test_obbt.py index 23a6c2bf783..bae429b3e9c 100644 --- a/pyomo/contrib/coramin/domain_reduction/tests/test_obbt.py +++ b/pyomo/contrib/coramin/domain_reduction/tests/test_obbt.py @@ -4,6 +4,10 @@ from pyomo.contrib import appsi +ipopt_available = appsi.solvers.Ipopt().available() + + +@unittest.skipUnless(ipopt_available, 'ipopt is not available') class TestBoundsTightener(unittest.TestCase): @classmethod def setUpClass(cls): From e07bae8e9a33297ed0e8cb15c99fd6817a223c14 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 4 Mar 2024 10:44:00 -0700 Subject: [PATCH 119/128] update tests --- .../contrib/coramin/utils/tests/test_compare_models.py | 10 +++++----- pyomo/contrib/simplification/simplify.py | 3 +++ 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/coramin/utils/tests/test_compare_models.py b/pyomo/contrib/coramin/utils/tests/test_compare_models.py index be3fadc078a..ddc8aa371e5 100644 --- a/pyomo/contrib/coramin/utils/tests/test_compare_models.py +++ b/pyomo/contrib/coramin/utils/tests/test_compare_models.py @@ -62,9 +62,9 @@ def test_presolve1(self): self.assertFalse(m.c3.active) self.assertTrue(m.c4.active) self.assertTrue( - self._compare_expressions(m.c1.body, [m.x1 + m.x2 + 1, m.x2 + m.x1 + 1]) + self._compare_expressions(m.c1.body, [m.x1 + m.x2 + 1, m.x2 + m.x1 + 1, 1 + m.x1 + m.x2, 1 + m.x2 + m.x1]) ) - self.assertTrue(self._compare_expressions(m.c2.body, [m.x2 + 1])) + self.assertTrue(self._compare_expressions(m.c2.body, [m.x2 + 1, 1 + m.x2])) self.assertEqual(m.c1.lb, 1) self.assertEqual(m.c1.ub, 1) self.assertEqual(m.c2.lb, 1) @@ -85,9 +85,9 @@ def test_presolve2(self): self.assertFalse(m.c3.active) self.assertFalse(m.c4.active) self.assertTrue( - self._compare_expressions(m.c1.body, [m.x1 + m.x2 + 1, m.x2 + m.x1 + 1]) + self._compare_expressions(m.c1.body, [m.x1 + m.x2 + 1, m.x2 + m.x1 + 1, 1 + m.x1 + m.x2, 1 + m.x2 + m.x1]) ) - self.assertTrue(self._compare_expressions(m.c2.body, [m.x2 + 1])) + self.assertTrue(self._compare_expressions(m.c2.body, [m.x2 + 1, 1 + m.x2])) self.assertEqual(m.c1.lb, 1) self.assertEqual(m.c1.ub, 1) self.assertEqual(m.c2.lb, 1) @@ -105,7 +105,7 @@ def test_presolve3(self): self.assertFalse(m.c2.active) self.assertFalse(m.c3.active) self.assertFalse(m.c4.active) - self.assertTrue(self._compare_expressions(m.c1.body, [m.x1 + 1])) + self.assertTrue(self._compare_expressions(m.c1.body, [m.x1 + 1, 1 + m.x1])) self.assertEqual(m.c1.lb, 1) self.assertEqual(m.c1.ub, 1) self.assertEqual(len(m.bound_constraints), 1) diff --git a/pyomo/contrib/simplification/simplify.py b/pyomo/contrib/simplification/simplify.py index b8cc4995f91..3e00d5729f3 100644 --- a/pyomo/contrib/simplification/simplify.py +++ b/pyomo/contrib/simplification/simplify.py @@ -12,6 +12,7 @@ from pyomo.core.expr.sympy_tools import sympy2pyomo_expression, sympyify_expression from pyomo.core.expr.numeric_expr import NumericExpression from pyomo.core.expr.numvalue import is_fixed, value +from pyomo.core.expr import native_numeric_types import logging import warnings @@ -28,6 +29,8 @@ def simplify_with_sympy(expr: NumericExpression): + if type(expr) in native_numeric_types: + return expr om, se = sympyify_expression(expr) se = se.simplify() new_expr = sympy2pyomo_expression(se, om) From 74f751f4b283d5a937c7aadf378c4465ef68375a Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 4 Mar 2024 10:55:25 -0700 Subject: [PATCH 120/128] run black --- pyomo/contrib/coramin/algorithms/bnb/tests/test_bnb.py | 5 ++++- .../contrib/coramin/algorithms/multitree/multitree.py | 2 +- .../algorithms/multitree/tests/test_multitree.py | 5 ++++- .../coramin/third_party/tests/test_minlplib_tools.py | 10 ++++++++-- .../contrib/coramin/utils/tests/test_compare_models.py | 10 ++++++++-- 5 files changed, 25 insertions(+), 7 deletions(-) diff --git a/pyomo/contrib/coramin/algorithms/bnb/tests/test_bnb.py b/pyomo/contrib/coramin/algorithms/bnb/tests/test_bnb.py index 5b2a3485077..033cc9ae991 100644 --- a/pyomo/contrib/coramin/algorithms/bnb/tests/test_bnb.py +++ b/pyomo/contrib/coramin/algorithms/bnb/tests/test_bnb.py @@ -1,7 +1,10 @@ import math from pyomo.contrib import coramin -from pyomo.contrib.coramin.third_party.minlplib_tools import get_minlplib, parse_osil_file +from pyomo.contrib.coramin.third_party.minlplib_tools import ( + get_minlplib, + parse_osil_file, +) from pyomo.common import unittest from pyomo.contrib import appsi import os diff --git a/pyomo/contrib/coramin/algorithms/multitree/multitree.py b/pyomo/contrib/coramin/algorithms/multitree/multitree.py index a1221a7d221..c76da140405 100644 --- a/pyomo/contrib/coramin/algorithms/multitree/multitree.py +++ b/pyomo/contrib/coramin/algorithms/multitree/multitree.py @@ -907,7 +907,7 @@ def solve( model, self._orig_var_map = get_clone_and_var_map(model) - # prevent the model from being garbage collected; + # prevent the model from being garbage collected; # otherwise the variables will become "unattached" self.truely_original_model = model diff --git a/pyomo/contrib/coramin/algorithms/multitree/tests/test_multitree.py b/pyomo/contrib/coramin/algorithms/multitree/tests/test_multitree.py index d16d14af027..75a5adb8498 100644 --- a/pyomo/contrib/coramin/algorithms/multitree/tests/test_multitree.py +++ b/pyomo/contrib/coramin/algorithms/multitree/tests/test_multitree.py @@ -1,7 +1,10 @@ import math from pyomo.contrib import coramin -from pyomo.contrib.coramin.third_party.minlplib_tools import get_minlplib, parse_osil_file +from pyomo.contrib.coramin.third_party.minlplib_tools import ( + get_minlplib, + parse_osil_file, +) from pyomo.common import unittest from pyomo.contrib import appsi import os diff --git a/pyomo/contrib/coramin/third_party/tests/test_minlplib_tools.py b/pyomo/contrib/coramin/third_party/tests/test_minlplib_tools.py index 4427f6ffc36..52bc1e43f02 100644 --- a/pyomo/contrib/coramin/third_party/tests/test_minlplib_tools.py +++ b/pyomo/contrib/coramin/third_party/tests/test_minlplib_tools.py @@ -27,7 +27,9 @@ def test_get_minlplib_instancedata(self): self.assertTrue( os.path.exists(os.path.join(current_dir, 'minlplib', 'instancedata.csv')) ) - cases = coramin.third_party.filter_minlplib_instances(instancedata_filename=fname) + cases = coramin.third_party.filter_minlplib_instances( + instancedata_filename=fname + ) self.assertEqual(len(cases), 1595 + 7) os.remove(os.path.join(current_dir, 'minlplib', 'instancedata.csv')) os.rmdir(os.path.join(current_dir, 'minlplib')) @@ -281,7 +283,11 @@ def test_get_minlplib(self): def test_get_minlplib_problem(self): current_dir = this_file_dir() - coramin.third_party.get_minlplib(download_dir=os.path.join(current_dir, 'minlplib', 'gms'), format='gms', problem_name='ex4_1_1') + coramin.third_party.get_minlplib( + download_dir=os.path.join(current_dir, 'minlplib', 'gms'), + format='gms', + problem_name='ex4_1_1', + ) files = os.listdir(os.path.join(current_dir, 'minlplib', 'gms')) self.assertEqual(len(files), 1) self.assertEqual(files[0], 'ex4_1_1.gms') diff --git a/pyomo/contrib/coramin/utils/tests/test_compare_models.py b/pyomo/contrib/coramin/utils/tests/test_compare_models.py index ddc8aa371e5..79b582b04fe 100644 --- a/pyomo/contrib/coramin/utils/tests/test_compare_models.py +++ b/pyomo/contrib/coramin/utils/tests/test_compare_models.py @@ -62,7 +62,10 @@ def test_presolve1(self): self.assertFalse(m.c3.active) self.assertTrue(m.c4.active) self.assertTrue( - self._compare_expressions(m.c1.body, [m.x1 + m.x2 + 1, m.x2 + m.x1 + 1, 1 + m.x1 + m.x2, 1 + m.x2 + m.x1]) + self._compare_expressions( + m.c1.body, + [m.x1 + m.x2 + 1, m.x2 + m.x1 + 1, 1 + m.x1 + m.x2, 1 + m.x2 + m.x1], + ) ) self.assertTrue(self._compare_expressions(m.c2.body, [m.x2 + 1, 1 + m.x2])) self.assertEqual(m.c1.lb, 1) @@ -85,7 +88,10 @@ def test_presolve2(self): self.assertFalse(m.c3.active) self.assertFalse(m.c4.active) self.assertTrue( - self._compare_expressions(m.c1.body, [m.x1 + m.x2 + 1, m.x2 + m.x1 + 1, 1 + m.x1 + m.x2, 1 + m.x2 + m.x1]) + self._compare_expressions( + m.c1.body, + [m.x1 + m.x2 + 1, m.x2 + m.x1 + 1, 1 + m.x1 + m.x2, 1 + m.x2 + m.x1], + ) ) self.assertTrue(self._compare_expressions(m.c2.body, [m.x2 + 1, 1 + m.x2])) self.assertEqual(m.c1.lb, 1) From 4b86141bb8e3b9f8f0dcddbca1b7c9ce89a86f84 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 4 Mar 2024 11:01:48 -0700 Subject: [PATCH 121/128] typo --- pyomo/contrib/coramin/algorithms/multitree/multitree.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/coramin/algorithms/multitree/multitree.py b/pyomo/contrib/coramin/algorithms/multitree/multitree.py index c76da140405..133c48d2643 100644 --- a/pyomo/contrib/coramin/algorithms/multitree/multitree.py +++ b/pyomo/contrib/coramin/algorithms/multitree/multitree.py @@ -909,7 +909,7 @@ def solve( # prevent the model from being garbage collected; # otherwise the variables will become "unattached" - self.truely_original_model = model + self.truly_original_model = model self._original_model, self._relaxation = clone_shallow_active_flat(model, 2) model = self._original_model From 45a27bfecc56da407d0b830666c75acd22173479 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 4 Mar 2024 12:10:09 -0700 Subject: [PATCH 122/128] add some __init__ files --- pyomo/contrib/coramin/algorithms/tests/__init__.py | 0 pyomo/contrib/coramin/domain_reduction/tests/__init__.py | 0 pyomo/contrib/coramin/examples/__init__.py | 0 pyomo/contrib/coramin/relaxations/tests/__init__.py | 0 pyomo/contrib/coramin/third_party/tests/__init__.py | 0 5 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 pyomo/contrib/coramin/algorithms/tests/__init__.py create mode 100644 pyomo/contrib/coramin/domain_reduction/tests/__init__.py create mode 100644 pyomo/contrib/coramin/examples/__init__.py create mode 100644 pyomo/contrib/coramin/relaxations/tests/__init__.py create mode 100644 pyomo/contrib/coramin/third_party/tests/__init__.py diff --git a/pyomo/contrib/coramin/algorithms/tests/__init__.py b/pyomo/contrib/coramin/algorithms/tests/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/pyomo/contrib/coramin/domain_reduction/tests/__init__.py b/pyomo/contrib/coramin/domain_reduction/tests/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/pyomo/contrib/coramin/examples/__init__.py b/pyomo/contrib/coramin/examples/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/pyomo/contrib/coramin/relaxations/tests/__init__.py b/pyomo/contrib/coramin/relaxations/tests/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/pyomo/contrib/coramin/third_party/tests/__init__.py b/pyomo/contrib/coramin/third_party/tests/__init__.py new file mode 100644 index 00000000000..e69de29bb2d From 0f3b24533c2bc02ea653b57f9c5c4d37bceedb2e Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 4 Mar 2024 12:35:48 -0700 Subject: [PATCH 123/128] update imports --- pyomo/contrib/coramin/algorithms/bnb/bnb.py | 2 +- pyomo/contrib/coramin/algorithms/multitree/multitree.py | 2 +- pyomo/contrib/coramin/domain_reduction/dbt.py | 2 +- pyomo/contrib/coramin/domain_reduction/obbt.py | 2 +- pyomo/contrib/coramin/heuristics/diving.py | 2 +- pyomo/contrib/coramin/heuristics/feasibility_pump.py | 2 +- pyomo/contrib/coramin/relaxations/hessian.py | 2 +- pyomo/contrib/coramin/relaxations/tests/test_auto_relax.py | 2 +- pyomo/contrib/coramin/relaxations/tests/test_relaxations.py | 2 +- .../coramin/relaxations/tests/test_univariate_relaxations.py | 2 +- pyomo/contrib/coramin/relaxations/univariate.py | 2 +- pyomo/contrib/coramin/utils/mpi_utils.py | 2 +- pyomo/contrib/coramin/utils/plot_relaxation.py | 2 +- 13 files changed, 13 insertions(+), 13 deletions(-) diff --git a/pyomo/contrib/coramin/algorithms/bnb/bnb.py b/pyomo/contrib/coramin/algorithms/bnb/bnb.py index 3bb7fb87873..aa13afee396 100644 --- a/pyomo/contrib/coramin/algorithms/bnb/bnb.py +++ b/pyomo/contrib/coramin/algorithms/bnb/bnb.py @@ -16,7 +16,7 @@ from pyomo.contrib.coramin.cutting_planes.base import CutGenerator from typing import Tuple, List, Optional import math -import numpy as np +from pyomo.common.dependencies import numpy as np import logging from pyomo.contrib.appsi.base import ( Solver, diff --git a/pyomo/contrib/coramin/algorithms/multitree/multitree.py b/pyomo/contrib/coramin/algorithms/multitree/multitree.py index 133c48d2643..081ad0ad67e 100644 --- a/pyomo/contrib/coramin/algorithms/multitree/multitree.py +++ b/pyomo/contrib/coramin/algorithms/multitree/multitree.py @@ -42,7 +42,7 @@ from pyomo.common.modeling import unique_component_name from pyomo.common.errors import InfeasibleConstraintException from pyomo.contrib.fbbt.fbbt import BoundsManager -import numpy as np +from pyomo.common.dependencies import numpy as np from pyomo.core.expr.visitor import identify_variables from pyomo.contrib.coramin.clone import clone_shallow_active_flat, get_clone_and_var_map from pyomo.contrib.coramin.algorithms.cut_gen import AlphaBBConfig, find_cut_generators diff --git a/pyomo/contrib/coramin/domain_reduction/dbt.py b/pyomo/contrib/coramin/domain_reduction/dbt.py index d711866bdea..99b9024bd29 100644 --- a/pyomo/contrib/coramin/domain_reduction/dbt.py +++ b/pyomo/contrib/coramin/domain_reduction/dbt.py @@ -27,7 +27,7 @@ metis_available = True except ImportError: metis_available = False -import numpy as np +from pyomo.common.dependencies import numpy as np import math from pyomo.core.base.block import declare_custom_block, _BlockData from pyomo.contrib.coramin.utils.pyomo_utils import get_objective diff --git a/pyomo/contrib/coramin/domain_reduction/obbt.py b/pyomo/contrib/coramin/domain_reduction/obbt.py index b00f2b3ecf7..2e83acad830 100644 --- a/pyomo/contrib/coramin/domain_reduction/obbt.py +++ b/pyomo/contrib/coramin/domain_reduction/obbt.py @@ -7,7 +7,7 @@ from pyomo.contrib import appsi import logging import traceback -import numpy as np +from pyomo.common.dependencies import numpy as np import math import time from typing import Union, Sequence, Optional, List diff --git a/pyomo/contrib/coramin/heuristics/diving.py b/pyomo/contrib/coramin/heuristics/diving.py index 74727adad4d..d1d19b6aa69 100644 --- a/pyomo/contrib/coramin/heuristics/diving.py +++ b/pyomo/contrib/coramin/heuristics/diving.py @@ -5,7 +5,7 @@ from typing import Tuple, List, Sequence, Optional, MutableMapping from pyomo.contrib import appsi from pyomo.contrib.coramin.utils.pyomo_utils import get_objective -import numpy as np +from pyomo.common.dependencies import numpy as np import math from pyomo.common.collections import ComponentSet, ComponentMap from pyomo.core.expr.visitor import identify_variables diff --git a/pyomo/contrib/coramin/heuristics/feasibility_pump.py b/pyomo/contrib/coramin/heuristics/feasibility_pump.py index 03dc7d6f7b7..03455cc9e4c 100644 --- a/pyomo/contrib/coramin/heuristics/feasibility_pump.py +++ b/pyomo/contrib/coramin/heuristics/feasibility_pump.py @@ -10,7 +10,7 @@ import time from pyomo.common.modeling import unique_component_name import random -import numpy as np +from pyomo.common.dependencies import numpy as np def collect_integer_vars( diff --git a/pyomo/contrib/coramin/relaxations/hessian.py b/pyomo/contrib/coramin/relaxations/hessian.py index 05ccfa96c24..fa0a2650ef9 100644 --- a/pyomo/contrib/coramin/relaxations/hessian.py +++ b/pyomo/contrib/coramin/relaxations/hessian.py @@ -6,7 +6,7 @@ from pyomo.core.expr.numvalue import is_fixed import math from pyomo.contrib.fbbt.fbbt import compute_bounds_on_expr -import numpy as np +from pyomo.common.dependencies import numpy as np from pyomo.contrib.coramin.utils.coramin_enums import EigenValueBounder from pyomo.core.base.block import _BlockData from typing import Optional, MutableMapping diff --git a/pyomo/contrib/coramin/relaxations/tests/test_auto_relax.py b/pyomo/contrib/coramin/relaxations/tests/test_auto_relax.py index 17bbd085b2b..e6fd54ddd16 100644 --- a/pyomo/contrib/coramin/relaxations/tests/test_auto_relax.py +++ b/pyomo/contrib/coramin/relaxations/tests/test_auto_relax.py @@ -5,7 +5,7 @@ from pyomo.core.expr.visitor import identify_variables, identify_components import math from pyomo.common.collections import ComponentSet -import numpy as np +from pyomo.common.dependencies import numpy as np from pyomo.core.base.param import _ParamData, ScalarParam from pyomo.core.expr.sympy_tools import sympyify_expression from pyomo.contrib import appsi diff --git a/pyomo/contrib/coramin/relaxations/tests/test_relaxations.py b/pyomo/contrib/coramin/relaxations/tests/test_relaxations.py index f3d38aded82..5de31f81a62 100644 --- a/pyomo/contrib/coramin/relaxations/tests/test_relaxations.py +++ b/pyomo/contrib/coramin/relaxations/tests/test_relaxations.py @@ -5,7 +5,7 @@ from pyomo.core.base.var import _GeneralVarData from pyomo.core.expr.numeric_expr import ExpressionBase from typing import Sequence, List, Tuple -import numpy as np +from pyomo.common.dependencies import numpy as np import itertools from pyomo.contrib import appsi from pyomo.core.expr.visitor import identify_variables diff --git a/pyomo/contrib/coramin/relaxations/tests/test_univariate_relaxations.py b/pyomo/contrib/coramin/relaxations/tests/test_univariate_relaxations.py index 417b4b470cc..c96dd032c9b 100644 --- a/pyomo/contrib/coramin/relaxations/tests/test_univariate_relaxations.py +++ b/pyomo/contrib/coramin/relaxations/tests/test_univariate_relaxations.py @@ -2,7 +2,7 @@ import math import pyomo.environ as pe from pyomo.contrib import coramin -import numpy as np +from pyomo.common.dependencies import numpy as np from pyomo.contrib.coramin.relaxations.segments import compute_k_segment_points from pyomo.contrib import appsi diff --git a/pyomo/contrib/coramin/relaxations/univariate.py b/pyomo/contrib/coramin/relaxations/univariate.py index 4a839953552..17da092e041 100644 --- a/pyomo/contrib/coramin/relaxations/univariate.py +++ b/pyomo/contrib/coramin/relaxations/univariate.py @@ -2,7 +2,7 @@ from pyomo.contrib.coramin.utils.coramin_enums import RelaxationSide, FunctionShape from .relaxations_base import BasePWRelaxationData, _check_cut from .custom_block import declare_custom_block -import numpy as np +from pyomo.common.dependencies import numpy as np import math import scipy.optimize from ._utils import check_var_pts, _get_bnds_list, _get_bnds_tuple diff --git a/pyomo/contrib/coramin/utils/mpi_utils.py b/pyomo/contrib/coramin/utils/mpi_utils.py index b9dfa7ea75d..c711c8036f2 100644 --- a/pyomo/contrib/coramin/utils/mpi_utils.py +++ b/pyomo/contrib/coramin/utils/mpi_utils.py @@ -1,5 +1,5 @@ from mpi4py import MPI -import numpy as np +from pyomo.common.dependencies import numpy as np import sys import os diff --git a/pyomo/contrib/coramin/utils/plot_relaxation.py b/pyomo/contrib/coramin/utils/plot_relaxation.py index 0e48f2412ff..f5f4baef0c7 100644 --- a/pyomo/contrib/coramin/utils/plot_relaxation.py +++ b/pyomo/contrib/coramin/utils/plot_relaxation.py @@ -1,4 +1,4 @@ -import numpy as np +from pyomo.common.dependencies import numpy as np try: import plotly.graph_objects as go From 4a90f09cc30c479dbf9f9fcb2f8ec1fa0b3b25a9 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 4 Mar 2024 13:21:34 -0700 Subject: [PATCH 124/128] update imports --- pyomo/contrib/coramin/__init__.py | 10 ++++++++++ setup.py | 1 + 2 files changed, 11 insertions(+) diff --git a/pyomo/contrib/coramin/__init__.py b/pyomo/contrib/coramin/__init__.py index b87e7910841..492ee514ab8 100644 --- a/pyomo/contrib/coramin/__init__.py +++ b/pyomo/contrib/coramin/__init__.py @@ -1,3 +1,13 @@ +from pyomo.common.dependencies import numpy, numpy_available, attempt_import +from pyomo.common import unittest + +if not numpy_available: + raise unittest.SkipTest('numpy is not available') + +pybnb, pybnb_available = attempt_import('pybnb') +if not pybnb_available: + raise unittest.SkipTest('pybnb is not available') + from . import utils from . import domain_reduction from . import relaxations diff --git a/setup.py b/setup.py index 70c1626a650..90a79970e80 100644 --- a/setup.py +++ b/setup.py @@ -276,6 +276,7 @@ def __ne__(self, other): #'pathos', # requested for #963, but PR currently closed 'pint', # units 'plotly', # incidence_analysis + 'pybnb', # coramin 'python-louvain', # community_detection 'pyyaml', # core # qtconsole also requires a supported Qt version (PyQt5 or PySide6). From 21ee445fc56384980ec4c98295aa5c70b670158c Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 4 Mar 2024 13:26:21 -0700 Subject: [PATCH 125/128] update GHA --- .github/workflows/test_branches.yml | 2 +- .github/workflows/test_pr_and_main.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 01231ecaf42..2af1187cc51 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -21,7 +21,7 @@ defaults: env: PYTHONWARNINGS: ignore::UserWarning PYTHON_CORE_PKGS: wheel - PYPI_ONLY: z3-solver + PYPI_ONLY: z3-solver pybnb PYPY_EXCLUDE: scipy numdifftools seaborn statsmodels CACHE_VER: v221013.1 NEOS_EMAIL: tests@pyomo.org diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index a42da98a397..77d1ac0412d 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -24,7 +24,7 @@ defaults: env: PYTHONWARNINGS: ignore::UserWarning PYTHON_CORE_PKGS: wheel - PYPI_ONLY: z3-solver + PYPI_ONLY: z3-solver pybnb PYPY_EXCLUDE: scipy numdifftools seaborn statsmodels CACHE_VER: v221013.1 NEOS_EMAIL: tests@pyomo.org From 01cc572d4d0666b3213d63d25d084f01c72f4fd3 Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 4 Mar 2024 13:59:24 -0700 Subject: [PATCH 126/128] update imports --- pyomo/contrib/coramin/domain_reduction/dbt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/coramin/domain_reduction/dbt.py b/pyomo/contrib/coramin/domain_reduction/dbt.py index 99b9024bd29..984b7842c6b 100644 --- a/pyomo/contrib/coramin/domain_reduction/dbt.py +++ b/pyomo/contrib/coramin/domain_reduction/dbt.py @@ -25,7 +25,7 @@ import metis metis_available = True -except ImportError: +except: metis_available = False from pyomo.common.dependencies import numpy as np import math From 96e2b03c735c7e7f8ff3d0e06aee4c9f26aec99d Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Mon, 4 Mar 2024 16:43:41 -0700 Subject: [PATCH 127/128] update imports --- pyomo/contrib/coramin/domain_reduction/tests/test_dbt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/coramin/domain_reduction/tests/test_dbt.py b/pyomo/contrib/coramin/domain_reduction/tests/test_dbt.py index 753510fbec2..8cb3c24a8e7 100644 --- a/pyomo/contrib/coramin/domain_reduction/tests/test_dbt.py +++ b/pyomo/contrib/coramin/domain_reduction/tests/test_dbt.py @@ -31,7 +31,7 @@ import metis metis_available = True -except ImportError: +except: metis_available = False From e859b78db2f0fd95ed69e0d1370d93d7a190814c Mon Sep 17 00:00:00 2001 From: Michael Bynum Date: Tue, 23 Apr 2024 08:55:11 -0600 Subject: [PATCH 128/128] fix bug --- pyomo/contrib/coramin/relaxations/relaxations_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/coramin/relaxations/relaxations_base.py b/pyomo/contrib/coramin/relaxations/relaxations_base.py index 6baf3fb1e40..91f2db73761 100644 --- a/pyomo/contrib/coramin/relaxations/relaxations_base.py +++ b/pyomo/contrib/coramin/relaxations/relaxations_base.py @@ -309,7 +309,7 @@ def rebuild(self, build_nonlinear_constraint=False, ensure_oa_at_vertices=True): self._original_constraint = pe.Constraint( expr=self.get_aux_var() <= self.get_rhs_expr() ) - else: + elif not build_nonlinear_constraint: if self._has_a_convex_side(): if self.use_linear_relaxation: if self._cuts is None: