From 7d88fe4aee8a97e0e199e95c3f7e29064bccd3fa Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Mon, 26 Feb 2024 17:05:40 +0100 Subject: [PATCH 01/39] Added MAiNGO appsi-interface --- pyomo/contrib/appsi/solvers/__init__.py | 1 + pyomo/contrib/appsi/solvers/maingo.py | 653 ++++++++++++++++++++++++ 2 files changed, 654 insertions(+) create mode 100644 pyomo/contrib/appsi/solvers/maingo.py diff --git a/pyomo/contrib/appsi/solvers/__init__.py b/pyomo/contrib/appsi/solvers/__init__.py index c03523a69d4..c9e0a2a003d 100644 --- a/pyomo/contrib/appsi/solvers/__init__.py +++ b/pyomo/contrib/appsi/solvers/__init__.py @@ -15,3 +15,4 @@ from .cplex import Cplex from .highs import Highs from .wntr import Wntr, WntrResults +from .maingo import MAiNGO \ No newline at end of file diff --git a/pyomo/contrib/appsi/solvers/maingo.py b/pyomo/contrib/appsi/solvers/maingo.py new file mode 100644 index 00000000000..dcb8040eabe --- /dev/null +++ b/pyomo/contrib/appsi/solvers/maingo.py @@ -0,0 +1,653 @@ +from collections import namedtuple +import logging +import math +import sys +from typing import Optional, List, Dict + +from pyomo.contrib.appsi.base import ( + PersistentSolver, + Results, + TerminationCondition, + MIPSolverConfig, + PersistentBase, + PersistentSolutionLoader, +) +from pyomo.contrib.appsi.cmodel import cmodel, cmodel_available +from pyomo.common.collections import ComponentMap +from pyomo.common.config import ConfigValue, NonNegativeInt +from pyomo.common.dependencies import attempt_import +from pyomo.common.errors import PyomoException +from pyomo.common.log import LogStream +from pyomo.common.tee import capture_output, TeeStream +from pyomo.common.timing import HierarchicalTimer +from pyomo.core.base import SymbolMap, NumericLabeler, TextLabeler +from pyomo.core.base.constraint import _GeneralConstraintData +from pyomo.core.base.expression import ScalarExpression +from pyomo.core.base.param import _ParamData +from pyomo.core.base.sos import _SOSConstraintData +from pyomo.core.base.var import Var, _GeneralVarData +import pyomo.core.expr.expr_common as common +import pyomo.core.expr as EXPR +from pyomo.core.expr.numvalue import ( + value, + is_constant, + is_fixed, + native_numeric_types, + native_types, + nonpyomo_leaf_types, +) +from pyomo.core.kernel.objective import minimize, maximize +from pyomo.core.staleflag import StaleFlagManager +from pyomo.repn.util import valid_expr_ctypes_minlp + +_plusMinusOne = {-1, 1} + +MaingoVar = namedtuple("MaingoVar", "type name lb ub init") + +logger = logging.getLogger(__name__) + + +def _import_maingopy(): + try: + import maingopy + except ImportError: + MAiNGO._available = MAiNGO.Availability.NotFound + raise + return maingopy + + +maingopy, maingopy_available = attempt_import("maingopy", importer=_import_maingopy) + + +class MAiNGOConfig(MIPSolverConfig): + def __init__( + self, + description=None, + doc=None, + implicit=False, + implicit_domain=None, + visibility=0, + ): + super(MAiNGOConfig, self).__init__( + description=description, + doc=doc, + implicit=implicit, + implicit_domain=implicit_domain, + visibility=visibility, + ) + + self.declare("logfile", ConfigValue(domain=str)) + self.declare("solver_output_logger", ConfigValue()) + self.declare("log_level", ConfigValue(domain=NonNegativeInt)) + + self.logfile = "" + self.solver_output_logger = logger + self.log_level = logging.INFO + + +class MAiNGOSolutionLoader(PersistentSolutionLoader): + def load_vars(self, vars_to_load=None): + self._assert_solution_still_valid() + self._solver.load_vars(vars_to_load=vars_to_load) + + def get_primals(self, vars_to_load=None): + self._assert_solution_still_valid() + return self._solver.get_primals(vars_to_load=vars_to_load) + + +class MAiNGOResults(Results): + def __init__(self, solver): + super(MAiNGOResults, self).__init__() + self.wallclock_time = None + self.cpu_time = None + self.solution_loader = MAiNGOSolutionLoader(solver=solver) + + +class SolverModel(maingopy.MAiNGOmodel): + def __init__(self, var_list, objective, con_list, idmap): + maingopy.MAiNGOmodel.__init__(self) + self._var_list = var_list + self._con_list = con_list + self._objective = objective + self._idmap = idmap + + def build_maingo_objective(self, obj, visitor): + maingo_obj = visitor.dfs_postorder_stack(obj.expr) + if obj.sense == maximize: + maingo_obj *= -1 + return maingo_obj + + def build_maingo_constraints(self, cons, visitor): + eqs = [] + ineqs = [] + for con in cons: + if con.equality: + eqs += [visitor.dfs_postorder_stack(con.body - con.lower)] + elif con.has_ub() and con.has_lb(): + ineqs += [visitor.dfs_postorder_stack(con.body - con.upper)] + ineqs += [visitor.dfs_postorder_stack(con.lower - con.body)] + elif con.has_ub(): + ineqs += [visitor.dfs_postorder_stack(con.body - con.upper)] + elif con.has_ub(): + ineqs += [visitor.dfs_postorder_stack(con.lower - con.body)] + else: + raise ValueError( + "Constraint does not have a lower " + "or an upper bound: {0} \n".format(con) + ) + return eqs, ineqs + + def get_variables(self): + return [ + maingopy.OptimizationVariable( + maingopy.Bounds(var.lb, var.ub), var.type, var.name + ) + for var in self._var_list + ] + + def get_initial_point(self): + return [var.init if not var.init is None else var.lb for var in self._var_list] + + def evaluate(self, maingo_vars): + visitor = ToMAiNGOVisitor(maingo_vars, self._idmap) + result = maingopy.EvaluationContainer() + result.objective = self.build_maingo_objective(self._objective, visitor) + eqs, ineqs = self.build_maingo_constraints(self._con_list, visitor) + result.eq = eqs + result.ineq = ineqs + return result + + +LEFT_TO_RIGHT = common.OperatorAssociativity.LEFT_TO_RIGHT +RIGHT_TO_LEFT = common.OperatorAssociativity.RIGHT_TO_LEFT + + +class ToMAiNGOVisitor(EXPR.ExpressionValueVisitor): + def __init__(self, variables, idmap): + super(ToMAiNGOVisitor, self).__init__() + self.variables = variables + self.idmap = idmap + self._pyomo_func_to_maingo_func = { + "log": maingopy.log, + "log10": ToMAiNGOVisitor.maingo_log10, + "sin": maingopy.sin, + "cos": maingopy.cos, + "tan": maingopy.tan, + "cosh": maingopy.cosh, + "sinh": maingopy.sinh, + "tanh": maingopy.tanh, + "asin": maingopy.asin, + "acos": maingopy.acos, + "atan": maingopy.atan, + "exp": maingopy.exp, + "sqrt": maingopy.sqrt, + "asinh": ToMAiNGOVisitor.maingo_asinh, + "acosh": ToMAiNGOVisitor.maingo_acosh, + "atanh": ToMAiNGOVisitor.maingo_atanh, + } + + @classmethod + def maingo_log10(cls, x): + return maingopy.log(x) / math.log(10) + + @classmethod + def maingo_asinh(cls, x): + return maingopy.inv(maingopy.sinh(x)) + + @classmethod + def maingo_acosh(cls, x): + return maingopy.inv(maingopy.cosh(x)) + + @classmethod + def maingo_atanh(cls, x): + return maingopy.inv(maingopy.tanh(x)) + + def visit(self, node, values): + """Visit nodes that have been expanded""" + for i, val in enumerate(values): + arg = node._args_[i] + + if arg is None: + values[i] = "Undefined" + elif arg.__class__ in native_numeric_types: + pass + elif arg.__class__ in nonpyomo_leaf_types: + values[i] = val + else: + parens = False + if arg.is_expression_type() and node.PRECEDENCE is not None: + if arg.PRECEDENCE is None: + pass + elif node.PRECEDENCE < arg.PRECEDENCE: + parens = True + elif node.PRECEDENCE == arg.PRECEDENCE: + if i == 0: + parens = node.ASSOCIATIVITY != LEFT_TO_RIGHT + elif i == len(node._args_) - 1: + parens = node.ASSOCIATIVITY != RIGHT_TO_LEFT + else: + parens = True + if parens: + values[i] = val + + if node.__class__ in EXPR.NPV_expression_types: + return value(node) + + if node.__class__ in {EXPR.ProductExpression, EXPR.MonomialTermExpression}: + return values[0] * values[1] + + if node.__class__ in {EXPR.SumExpression}: + return sum(values) + + if node.__class__ in {EXPR.PowExpression}: + return maingopy.pow(values[0], values[1]) + + if node.__class__ in {EXPR.DivisionExpression}: + return values[0] / values[1] + + if node.__class__ in {EXPR.NegationExpression}: + return -values[0] + + if node.__class__ in {EXPR.AbsExpression}: + return maingopy.abs(values[0]) + + if node.__class__ in {EXPR.UnaryFunctionExpression}: + pyomo_func = node.getname() + maingo_func = self._pyomo_func_to_maingo_func[pyomo_func] + return maingo_func(values[0]) + + if node.__class__ in {ScalarExpression}: + return values[0] + + raise ValueError(f"Unknown function expression encountered: {node.getname()}") + + def visiting_potential_leaf(self, node): + """ + Visiting a potential leaf. + + Return True if the node is not expanded. + """ + if node.__class__ in native_types: + return True, node + + if node.is_expression_type(): + if node.__class__ is EXPR.MonomialTermExpression: + return True, self._monomial_to_maingo(node) + if node.__class__ is EXPR.LinearExpression: + return True, self._linear_to_maingo(node) + return False, None + + if node.is_component_type(): + if node.ctype not in valid_expr_ctypes_minlp: + # Make sure all components in active constraints + # are basic ctypes we know how to deal with. + raise RuntimeError( + "Unallowable component '%s' of type %s found in an active " + "constraint or objective.\nMAiNGO cannot export " + "expressions with this component type." + % (node.name, node.ctype.__name__) + ) + + if node.is_fixed(): + return True, node() + else: + assert node.is_variable_type() + maingo_var_id = self.idmap[id(node)] + maingo_var = self.variables[maingo_var_id] + return True, maingo_var + + def _monomial_to_maingo(self, node): + const, var = node.args + maingo_var_id = self.idmap[id(var)] + maingo_var = self.variables[maingo_var_id] + if const.__class__ not in native_types: + const = value(const) + if var.is_fixed(): + return const * var.value + if not const: + return 0 + if const in _plusMinusOne: + if const < 0: + return -maingo_var + else: + return maingo_var + return const * maingo_var + + def _linear_to_maingo(self, node): + values = [ + self._monomial_to_maingo(arg) + if ( + arg.__class__ is EXPR.MonomialTermExpression + and not arg.arg(1).is_fixed() + ) + else value(arg) + for arg in node.args + ] + return sum(values) + + +class MAiNGO(PersistentBase, PersistentSolver): + """ + Interface to MAiNGO + """ + + _available = None + + def __init__(self, only_child_vars=False): + super(MAiNGO, self).__init__(only_child_vars=only_child_vars) + self._config = MAiNGOConfig() + self._solver_options = dict() + self._solver_model = None + self._mymaingo = None + self._symbol_map = SymbolMap() + self._labeler = None + self._maingo_vars = [] + self._objective = None + self._cons = [] + self._pyomo_var_to_solver_var_id_map = dict() + self._last_results_object: Optional[MAiNGOResults] = None + + def available(self): + if not maingopy_available: + return self.Availability.NotFound + self._available = True + return self._available + + def version(self): + pass + + @property + def config(self) -> MAiNGOConfig: + return self._config + + @config.setter + def config(self, val: MAiNGOConfig): + self._config = val + + @property + def maingo_options(self): + """ + A dictionary mapping solver options to values for those options. These + are solver specific. + + Returns + ------- + dict + A dictionary mapping solver options to values for those options + """ + return self._solver_options + + @maingo_options.setter + def maingo_options(self, val: Dict): + self._solver_options = val + + @property + def symbol_map(self): + return self._symbol_map + + def _solve(self, timer: HierarchicalTimer): + ostreams = [ + LogStream( + level=self.config.log_level, logger=self.config.solver_output_logger + ) + ] + if self.config.stream_solver: + ostreams.append(sys.stdout) + + with TeeStream(*ostreams) as t: + with capture_output(output=t.STDOUT, capture_fd=False): + config = self.config + options = self.maingo_options + + self._mymaingo = maingopy.MAiNGO(self._solver_model) + + self._mymaingo.set_option("loggingDestination", 2) + self._mymaingo.set_log_file_name(config.logfile) + + if config.time_limit is not None: + self._mymaingo.set_option("maxTime", config.time_limit) + if config.mip_gap is not None: + self._mymaingo.set_option("epsilonA", config.mip_gap) + for key, option in options.items(): + self._mymaingo.set_option(key, option) + + timer.start("MAiNGO solve") + self._mymaingo.solve() + timer.stop("MAiNGO solve") + + return self._postsolve(timer) + + def solve(self, model, timer: HierarchicalTimer = None): + StaleFlagManager.mark_all_as_stale() + + if self._last_results_object is not None: + self._last_results_object.solution_loader.invalidate() + if timer is None: + timer = HierarchicalTimer() + timer.start("set_instance") + self.set_instance(model) + timer.stop("set_instance") + res = self._solve(timer) + self._last_results_object = res + if self.config.report_timing: + logger.info("\n" + str(timer)) + return res + + def _process_domain_and_bounds(self, var): + _v, _lb, _ub, _fixed, _domain_interval, _value = self._vars[id(var)] + lb, ub, step = _domain_interval + if lb is None: + lb = -1e10 + if ub is None: + ub = 1e10 + if step == 0: + vtype = maingopy.VT_CONTINUOUS + elif step == 1: + if lb == 0 and ub == 1: + vtype = maingopy.VT_BINARY + else: + vtype = maingopy.VT_INTEGER + else: + raise ValueError( + f"Unrecognized domain step: {step} (should be either 0 or 1)" + ) + if _fixed: + lb = _value + ub = _value + else: + if _lb is not None: + lb = max(value(_lb), lb) + if _ub is not None: + ub = min(value(_ub), ub) + + return lb, ub, vtype + + def _add_variables(self, variables: List[_GeneralVarData]): + for ndx, var in enumerate(variables): + varname = self._symbol_map.getSymbol(var, self._labeler) + lb, ub, vtype = self._process_domain_and_bounds(var) + self._maingo_vars.append( + MaingoVar(name=varname, type=vtype, lb=lb, ub=ub, init=var.value) + ) + self._pyomo_var_to_solver_var_id_map[id(var)] = len(self._maingo_vars) - 1 + + def _add_params(self, params: List[_ParamData]): + pass + + def _reinit(self): + saved_config = self.config + saved_options = self.maingo_options + saved_update_config = self.update_config + self.__init__(only_child_vars=self._only_child_vars) + self.config = saved_config + self.maingo_options = saved_options + self.update_config = saved_update_config + + def set_instance(self, model): + if self._last_results_object is not None: + self._last_results_object.solution_loader.invalidate() + if not self.available(): + c = self.__class__ + raise PyomoException( + f"Solver {c.__module__}.{c.__qualname__} is not available " + f"({self.available()})." + ) + self._reinit() + self._model = model + if self.use_extensions and cmodel_available: + self._expr_types = cmodel.PyomoExprTypes() + + if self.config.symbolic_solver_labels: + self._labeler = TextLabeler() + else: + self._labeler = NumericLabeler("x") + + self.add_block(model) + self._solver_model = SolverModel( + var_list=self._maingo_vars, + con_list=self._cons, + objective=self._objective, + idmap=self._pyomo_var_to_solver_var_id_map, + ) + + def _add_constraints(self, cons: List[_GeneralConstraintData]): + self._cons = cons + + def _add_sos_constraints(self, cons: List[_SOSConstraintData]): + pass + + def _remove_constraints(self, cons: List[_GeneralConstraintData]): + pass + + def _remove_sos_constraints(self, cons: List[_SOSConstraintData]): + pass + + def _remove_variables(self, variables: List[_GeneralVarData]): + pass + + def _remove_params(self, params: List[_ParamData]): + pass + + def _update_variables(self, variables: List[_GeneralVarData]): + pass + + def update_params(self): + pass + + def _set_objective(self, obj): + if obj is None: + raise NotImplementedError( + "MAiNGO needs a objective. Please set a dummy objective." + ) + else: + if not obj.sense in {minimize, maximize}: + raise ValueError( + "Objective sense is not recognized: {0}".format(obj.sense) + ) + self._objective = obj + + def _postsolve(self, timer: HierarchicalTimer): + config = self.config + + mprob = self._mymaingo + status = mprob.get_status() + results = MAiNGOResults(solver=self) + results.wallclock_time = mprob.get_wallclock_solution_time() + results.cpu_time = mprob.get_cpu_solution_time() + + if status == maingopy.GLOBALLY_OPTIMAL: + results.termination_condition = TerminationCondition.optimal + elif status == maingopy.INFEASIBLE: + results.termination_condition = TerminationCondition.infeasible + else: + results.termination_condition = TerminationCondition.unknown + + results.best_feasible_objective = None + results.best_objective_bound = None + if self._objective is not None: + try: + if self._objective.sense == maximize: + results.best_feasible_objective = -mprob.get_objective_value() + else: + results.best_feasible_objective = mprob.get_objective_value() + except: + results.best_feasible_objective = None + try: + if self._objective.sense == maximize: + results.best_objective_bound = -mprob.get_final_LBD() + else: + results.best_objective_bound = mprob.get_final_LBD() + except: + if self._objective.sense == maximize: + results.best_objective_bound = math.inf + else: + results.best_objective_bound = -math.inf + + if results.best_feasible_objective is not None and not math.isfinite( + results.best_feasible_objective + ): + results.best_feasible_objective = None + + timer.start("load solution") + if config.load_solution: + if not results.best_feasible_objective is None: + if results.termination_condition != TerminationCondition.optimal: + logger.warning( + "Loading a feasible but suboptimal solution. " + "Please set load_solution=False and check " + "results.termination_condition and " + "results.found_feasible_solution() before loading a solution." + ) + self.load_vars() + else: + 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." + ) + timer.stop("load solution") + + return results + + def load_vars(self, vars_to_load=None): + for v, val in self.get_primals(vars_to_load=vars_to_load).items(): + v.set_value(val, skip_validation=True) + StaleFlagManager.mark_all_as_stale(delayed=True) + + def get_primals(self, vars_to_load=None): + if not self._mymaingo.get_status() in { + maingopy.GLOBALLY_OPTIMAL, + maingopy.FEASIBLE_POINT, + }: + raise RuntimeError( + "Solver does not currently have a valid solution." + "Please check the termination condition." + ) + + var_id_map = self._pyomo_var_to_solver_var_id_map + ref_vars = self._referenced_variables + if vars_to_load is None: + vars_to_load = var_id_map.keys() + else: + vars_to_load = [id(v) for v in vars_to_load] + + maingo_var_ids_to_load = [ + var_id_map[pyomo_var_id] for pyomo_var_id in vars_to_load + ] + + solution_point = self._mymaingo.get_solution_point() + vals = [solution_point[var_id] for var_id in maingo_var_ids_to_load] + + res = ComponentMap() + for var_id, val in zip(vars_to_load, vals): + using_cons, using_sos, using_obj = ref_vars[var_id] + if using_cons or using_sos or (using_obj is not None): + res[self._vars[var_id][0]] = val + return res + + def get_reduced_costs(self, vars_to_load=None): + raise ValueError("MAiNGO does not support returning Reduced Costs") + + def get_duals(self, cons_to_load=None): + raise ValueError("MAiNGO does not support returning Duals") From caa688ed390e609b78ff3af305f87223dd3a69e6 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Wed, 28 Feb 2024 11:12:00 +0100 Subject: [PATCH 02/39] Initial point calculation consistent with ALE syntax --- pyomo/contrib/appsi/solvers/maingo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/appsi/solvers/maingo.py b/pyomo/contrib/appsi/solvers/maingo.py index dcb8040eabe..530521f6b83 100644 --- a/pyomo/contrib/appsi/solvers/maingo.py +++ b/pyomo/contrib/appsi/solvers/maingo.py @@ -146,7 +146,7 @@ def get_variables(self): ] def get_initial_point(self): - return [var.init if not var.init is None else var.lb for var in self._var_list] + return [var.init if not var.init is None else (var.lb + var.ub)/2.0 for var in self._var_list] def evaluate(self, maingo_vars): visitor = ToMAiNGOVisitor(maingo_vars, self._idmap) From 365370a4220ff2fe0df818878f89b5f6fe36d7a3 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Wed, 28 Feb 2024 11:24:17 +0100 Subject: [PATCH 03/39] Added warning for missing variable bounds --- pyomo/contrib/appsi/solvers/maingo.py | 35 ++++++++++++++++++--------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/maingo.py b/pyomo/contrib/appsi/solvers/maingo.py index 530521f6b83..52b12d434ef 100644 --- a/pyomo/contrib/appsi/solvers/maingo.py +++ b/pyomo/contrib/appsi/solvers/maingo.py @@ -436,10 +436,28 @@ def solve(self, model, timer: HierarchicalTimer = None): def _process_domain_and_bounds(self, var): _v, _lb, _ub, _fixed, _domain_interval, _value = self._vars[id(var)] lb, ub, step = _domain_interval - if lb is None: - lb = -1e10 - if ub is None: - ub = 1e10 + + if _fixed: + lb = _value + ub = _value + else: + if lb is None and _lb is None: + logger.warning("No lower bound for variable " + var.getname() + " set. Using -1e10 instead. Please consider setting a valid lower bound.") + if ub is None and _ub is None: + logger.warning("No upper bound for variable " + var.getname() + " set. Using +1e10 instead. Please consider setting a valid upper bound.") + + if _lb is None: + _lb = -1e10 + if _ub is None: + _ub = 1e10 + if lb is None: + lb = -1e10 + if ub is None: + ub = 1e10 + + lb = max(value(_lb), lb) + ub = min(value(_ub), ub) + if step == 0: vtype = maingopy.VT_CONTINUOUS elif step == 1: @@ -451,14 +469,7 @@ def _process_domain_and_bounds(self, var): raise ValueError( f"Unrecognized domain step: {step} (should be either 0 or 1)" ) - if _fixed: - lb = _value - ub = _value - else: - if _lb is not None: - lb = max(value(_lb), lb) - if _ub is not None: - ub = min(value(_ub), ub) + return lb, ub, vtype From c440037d95146caaa896404235ebd2b60f8d8926 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Wed, 28 Feb 2024 11:32:55 +0100 Subject: [PATCH 04/39] Added: NotImplementedError for SOS constraints --- pyomo/contrib/appsi/solvers/maingo.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pyomo/contrib/appsi/solvers/maingo.py b/pyomo/contrib/appsi/solvers/maingo.py index 52b12d434ef..99cff4b7aa9 100644 --- a/pyomo/contrib/appsi/solvers/maingo.py +++ b/pyomo/contrib/appsi/solvers/maingo.py @@ -525,12 +525,16 @@ def _add_constraints(self, cons: List[_GeneralConstraintData]): self._cons = cons def _add_sos_constraints(self, cons: List[_SOSConstraintData]): + if len(cons) >= 1: + raise NotImplementedError("MAiNGO does not currently support SOS constraints.") pass def _remove_constraints(self, cons: List[_GeneralConstraintData]): pass def _remove_sos_constraints(self, cons: List[_SOSConstraintData]): + if len(cons) >= 1: + raise NotImplementedError("MAiNGO does not currently support SOS constraints.") pass def _remove_variables(self, variables: List[_GeneralVarData]): From 52a4cd9e59baf27bdab4df32b3a850d511ce894b Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Wed, 28 Feb 2024 11:39:28 +0100 Subject: [PATCH 05/39] Changed: Formulation of asinh, acosh, atanh --- pyomo/contrib/appsi/solvers/maingo.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/maingo.py b/pyomo/contrib/appsi/solvers/maingo.py index 99cff4b7aa9..099865f5a84 100644 --- a/pyomo/contrib/appsi/solvers/maingo.py +++ b/pyomo/contrib/appsi/solvers/maingo.py @@ -192,15 +192,15 @@ def maingo_log10(cls, x): @classmethod def maingo_asinh(cls, x): - return maingopy.inv(maingopy.sinh(x)) + return maingopy.log(x + maingopy.sqrt(maingopy.pow(x,2) + 1)) @classmethod def maingo_acosh(cls, x): - return maingopy.inv(maingopy.cosh(x)) + return maingopy.log(x + maingopy.sqrt(maingopy.pow(x,2) - 1)) @classmethod def maingo_atanh(cls, x): - return maingopy.inv(maingopy.tanh(x)) + return 0.5 * maingopy.log(x+1) - 0.5 * maingopy.log(1-x) def visit(self, node, values): """Visit nodes that have been expanded""" From 44311203d1b426a0894e8f9c7efbdbfa3c0eec85 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Wed, 28 Feb 2024 11:49:15 +0100 Subject: [PATCH 06/39] Added: Warning for non-global solutions --- pyomo/contrib/appsi/solvers/maingo.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/appsi/solvers/maingo.py b/pyomo/contrib/appsi/solvers/maingo.py index 099865f5a84..7a153f938b7 100644 --- a/pyomo/contrib/appsi/solvers/maingo.py +++ b/pyomo/contrib/appsi/solvers/maingo.py @@ -570,8 +570,10 @@ def _postsolve(self, timer: HierarchicalTimer): results.wallclock_time = mprob.get_wallclock_solution_time() results.cpu_time = mprob.get_cpu_solution_time() - if status == maingopy.GLOBALLY_OPTIMAL: + if status in {maingopy.GLOBALLY_OPTIMAL, maingopy.FEASIBLE_POINT}: results.termination_condition = TerminationCondition.optimal + if status == maingopy.FEASIBLE_POINT: + logger.warning("MAiNGO did only find a feasible solution but did not prove its global optimality.") elif status == maingopy.INFEASIBLE: results.termination_condition = TerminationCondition.infeasible else: From ff2c9bd86eefe7b2693f60a165383b525a764984 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Wed, 28 Feb 2024 11:57:01 +0100 Subject: [PATCH 07/39] Changed: absolute to relative MIP gap --- pyomo/contrib/appsi/solvers/maingo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/appsi/solvers/maingo.py b/pyomo/contrib/appsi/solvers/maingo.py index 7a153f938b7..cf09b42fbda 100644 --- a/pyomo/contrib/appsi/solvers/maingo.py +++ b/pyomo/contrib/appsi/solvers/maingo.py @@ -407,7 +407,7 @@ def _solve(self, timer: HierarchicalTimer): if config.time_limit is not None: self._mymaingo.set_option("maxTime", config.time_limit) if config.mip_gap is not None: - self._mymaingo.set_option("epsilonA", config.mip_gap) + self._mymaingo.set_option("epsilonR", config.mip_gap) for key, option in options.items(): self._mymaingo.set_option(key, option) From b833ac729aa7741331811c133d54d809f27e17be Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Wed, 28 Feb 2024 12:11:48 +0100 Subject: [PATCH 08/39] Added: Maingopy version --- pyomo/contrib/appsi/solvers/maingo.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/appsi/solvers/maingo.py b/pyomo/contrib/appsi/solvers/maingo.py index cf09b42fbda..05c5f50a295 100644 --- a/pyomo/contrib/appsi/solvers/maingo.py +++ b/pyomo/contrib/appsi/solvers/maingo.py @@ -354,7 +354,15 @@ def available(self): return self._available def version(self): - pass + # Check if Python >= 3.8 + if sys.version_info.major >= 3 and sys.version_info.minor >= 8: + from importlib.metadata import version + version = version('maingopy') + else: + import pkg_resources + version = pkg_resources.get_distribution('maingopy').version + + return tuple(int(k) for k in version.split('.')) @property def config(self) -> MAiNGOConfig: From c937b63e2e6e699efe24ef0e46cd36fd56da8134 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Wed, 28 Feb 2024 12:21:12 +0100 Subject: [PATCH 09/39] Black Formatting --- pyomo/contrib/appsi/solvers/__init__.py | 2 +- pyomo/contrib/appsi/solvers/maingo.py | 42 ++++++++++++++++++------- 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/__init__.py b/pyomo/contrib/appsi/solvers/__init__.py index c9e0a2a003d..352571b98f8 100644 --- a/pyomo/contrib/appsi/solvers/__init__.py +++ b/pyomo/contrib/appsi/solvers/__init__.py @@ -15,4 +15,4 @@ from .cplex import Cplex from .highs import Highs from .wntr import Wntr, WntrResults -from .maingo import MAiNGO \ No newline at end of file +from .maingo import MAiNGO diff --git a/pyomo/contrib/appsi/solvers/maingo.py b/pyomo/contrib/appsi/solvers/maingo.py index 05c5f50a295..d98b33af998 100644 --- a/pyomo/contrib/appsi/solvers/maingo.py +++ b/pyomo/contrib/appsi/solvers/maingo.py @@ -146,7 +146,10 @@ def get_variables(self): ] def get_initial_point(self): - return [var.init if not var.init is None else (var.lb + var.ub)/2.0 for var in self._var_list] + return [ + var.init if not var.init is None else (var.lb + var.ub) / 2.0 + for var in self._var_list + ] def evaluate(self, maingo_vars): visitor = ToMAiNGOVisitor(maingo_vars, self._idmap) @@ -192,15 +195,15 @@ def maingo_log10(cls, x): @classmethod def maingo_asinh(cls, x): - return maingopy.log(x + maingopy.sqrt(maingopy.pow(x,2) + 1)) + return maingopy.log(x + maingopy.sqrt(maingopy.pow(x, 2) + 1)) @classmethod def maingo_acosh(cls, x): - return maingopy.log(x + maingopy.sqrt(maingopy.pow(x,2) - 1)) + return maingopy.log(x + maingopy.sqrt(maingopy.pow(x, 2) - 1)) @classmethod def maingo_atanh(cls, x): - return 0.5 * maingopy.log(x+1) - 0.5 * maingopy.log(1-x) + return 0.5 * maingopy.log(x + 1) - 0.5 * maingopy.log(1 - x) def visit(self, node, values): """Visit nodes that have been expanded""" @@ -357,11 +360,13 @@ def version(self): # Check if Python >= 3.8 if sys.version_info.major >= 3 and sys.version_info.minor >= 8: from importlib.metadata import version + version = version('maingopy') else: import pkg_resources + version = pkg_resources.get_distribution('maingopy').version - + return tuple(int(k) for k in version.split('.')) @property @@ -450,10 +455,18 @@ def _process_domain_and_bounds(self, var): ub = _value else: if lb is None and _lb is None: - logger.warning("No lower bound for variable " + var.getname() + " set. Using -1e10 instead. Please consider setting a valid lower bound.") + logger.warning( + "No lower bound for variable " + + var.getname() + + " set. Using -1e10 instead. Please consider setting a valid lower bound." + ) if ub is None and _ub is None: - logger.warning("No upper bound for variable " + var.getname() + " set. Using +1e10 instead. Please consider setting a valid upper bound.") - + logger.warning( + "No upper bound for variable " + + var.getname() + + " set. Using +1e10 instead. Please consider setting a valid upper bound." + ) + if _lb is None: _lb = -1e10 if _ub is None: @@ -478,7 +491,6 @@ def _process_domain_and_bounds(self, var): f"Unrecognized domain step: {step} (should be either 0 or 1)" ) - return lb, ub, vtype def _add_variables(self, variables: List[_GeneralVarData]): @@ -534,7 +546,9 @@ def _add_constraints(self, cons: List[_GeneralConstraintData]): def _add_sos_constraints(self, cons: List[_SOSConstraintData]): if len(cons) >= 1: - raise NotImplementedError("MAiNGO does not currently support SOS constraints.") + raise NotImplementedError( + "MAiNGO does not currently support SOS constraints." + ) pass def _remove_constraints(self, cons: List[_GeneralConstraintData]): @@ -542,7 +556,9 @@ def _remove_constraints(self, cons: List[_GeneralConstraintData]): def _remove_sos_constraints(self, cons: List[_SOSConstraintData]): if len(cons) >= 1: - raise NotImplementedError("MAiNGO does not currently support SOS constraints.") + raise NotImplementedError( + "MAiNGO does not currently support SOS constraints." + ) pass def _remove_variables(self, variables: List[_GeneralVarData]): @@ -581,7 +597,9 @@ def _postsolve(self, timer: HierarchicalTimer): if status in {maingopy.GLOBALLY_OPTIMAL, maingopy.FEASIBLE_POINT}: results.termination_condition = TerminationCondition.optimal if status == maingopy.FEASIBLE_POINT: - logger.warning("MAiNGO did only find a feasible solution but did not prove its global optimality.") + logger.warning( + "MAiNGO did only find a feasible solution but did not prove its global optimality." + ) elif status == maingopy.INFEASIBLE: results.termination_condition = TerminationCondition.infeasible else: From ccb7723466f0bb06eb3abf552c44c084932704b3 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Wed, 28 Feb 2024 12:44:02 +0100 Subject: [PATCH 10/39] Fixed: Black Formatting --- pyomo/contrib/appsi/solvers/maingo.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/maingo.py b/pyomo/contrib/appsi/solvers/maingo.py index d98b33af998..614e12d227b 100644 --- a/pyomo/contrib/appsi/solvers/maingo.py +++ b/pyomo/contrib/appsi/solvers/maingo.py @@ -318,12 +318,14 @@ def _monomial_to_maingo(self, node): def _linear_to_maingo(self, node): values = [ - self._monomial_to_maingo(arg) - if ( - arg.__class__ is EXPR.MonomialTermExpression - and not arg.arg(1).is_fixed() + ( + self._monomial_to_maingo(arg) + if ( + arg.__class__ is EXPR.MonomialTermExpression + and not arg.arg(1).is_fixed() + ) + else value(arg) ) - else value(arg) for arg in node.args ] return sum(values) From e28e14db71b7ee09a54af2882d80837289f5c448 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Wed, 28 Feb 2024 14:33:06 +0100 Subject: [PATCH 11/39] Added: pip install maingopy to test_branches.yml --- .github/workflows/test_branches.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 77f47b505ff..1441cd53623 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -268,6 +268,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 maingopy \ + || echo "WARNING: MAiNGO is not available" if [[ ${{matrix.python}} == pypy* ]]; then echo "skipping wntr for pypy" else From c52bbc7f3c565d5d50ff98187efdb8d25de32e4b Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Wed, 28 Feb 2024 14:35:41 +0100 Subject: [PATCH 12/39] Added: pip install maingopy to test_pr_and_main.yml --- .github/workflows/test_pr_and_main.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index 87d6aa4d7a8..0214442d4e5 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -298,6 +298,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 maingopy \ + || echo "WARNING: MAiNGO is not available" if [[ ${{matrix.python}} == pypy* ]]; then echo "skipping wntr for pypy" else From db5d6dc96181f734f5f3410987ce4c0a1e3a6e7b Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Wed, 20 Mar 2024 10:53:00 +0100 Subject: [PATCH 13/39] Fixed maingopy import --- pyomo/contrib/appsi/solvers/maingo.py | 246 +---------------- .../appsi/solvers/maingo_solvermodel.py | 257 ++++++++++++++++++ 2 files changed, 272 insertions(+), 231 deletions(-) create mode 100644 pyomo/contrib/appsi/solvers/maingo_solvermodel.py diff --git a/pyomo/contrib/appsi/solvers/maingo.py b/pyomo/contrib/appsi/solvers/maingo.py index 614e12d227b..29464e6a876 100644 --- a/pyomo/contrib/appsi/solvers/maingo.py +++ b/pyomo/contrib/appsi/solvers/maingo.py @@ -25,7 +25,7 @@ from pyomo.core.base.expression import ScalarExpression from pyomo.core.base.param import _ParamData from pyomo.core.base.sos import _SOSConstraintData -from pyomo.core.base.var import Var, _GeneralVarData +from pyomo.core.base.var import Var, ScalarVar, _GeneralVarData import pyomo.core.expr.expr_common as common import pyomo.core.expr as EXPR from pyomo.core.expr.numvalue import ( @@ -40,7 +40,18 @@ from pyomo.core.staleflag import StaleFlagManager from pyomo.repn.util import valid_expr_ctypes_minlp -_plusMinusOne = {-1, 1} + +def _import_SolverModel(): + try: + from . import maingo_solvermodel + except ImportError: + raise + return maingo_solvermodel + + +maingo_solvermodel, solvermodel_available = attempt_import( + "maingo_solvermodel", importer=_import_SolverModel +) MaingoVar = namedtuple("MaingoVar", "type name lb ub init") @@ -103,234 +114,6 @@ def __init__(self, solver): self.solution_loader = MAiNGOSolutionLoader(solver=solver) -class SolverModel(maingopy.MAiNGOmodel): - def __init__(self, var_list, objective, con_list, idmap): - maingopy.MAiNGOmodel.__init__(self) - self._var_list = var_list - self._con_list = con_list - self._objective = objective - self._idmap = idmap - - def build_maingo_objective(self, obj, visitor): - maingo_obj = visitor.dfs_postorder_stack(obj.expr) - if obj.sense == maximize: - maingo_obj *= -1 - return maingo_obj - - def build_maingo_constraints(self, cons, visitor): - eqs = [] - ineqs = [] - for con in cons: - if con.equality: - eqs += [visitor.dfs_postorder_stack(con.body - con.lower)] - elif con.has_ub() and con.has_lb(): - ineqs += [visitor.dfs_postorder_stack(con.body - con.upper)] - ineqs += [visitor.dfs_postorder_stack(con.lower - con.body)] - elif con.has_ub(): - ineqs += [visitor.dfs_postorder_stack(con.body - con.upper)] - elif con.has_ub(): - ineqs += [visitor.dfs_postorder_stack(con.lower - con.body)] - else: - raise ValueError( - "Constraint does not have a lower " - "or an upper bound: {0} \n".format(con) - ) - return eqs, ineqs - - def get_variables(self): - return [ - maingopy.OptimizationVariable( - maingopy.Bounds(var.lb, var.ub), var.type, var.name - ) - for var in self._var_list - ] - - def get_initial_point(self): - return [ - var.init if not var.init is None else (var.lb + var.ub) / 2.0 - for var in self._var_list - ] - - def evaluate(self, maingo_vars): - visitor = ToMAiNGOVisitor(maingo_vars, self._idmap) - result = maingopy.EvaluationContainer() - result.objective = self.build_maingo_objective(self._objective, visitor) - eqs, ineqs = self.build_maingo_constraints(self._con_list, visitor) - result.eq = eqs - result.ineq = ineqs - return result - - -LEFT_TO_RIGHT = common.OperatorAssociativity.LEFT_TO_RIGHT -RIGHT_TO_LEFT = common.OperatorAssociativity.RIGHT_TO_LEFT - - -class ToMAiNGOVisitor(EXPR.ExpressionValueVisitor): - def __init__(self, variables, idmap): - super(ToMAiNGOVisitor, self).__init__() - self.variables = variables - self.idmap = idmap - self._pyomo_func_to_maingo_func = { - "log": maingopy.log, - "log10": ToMAiNGOVisitor.maingo_log10, - "sin": maingopy.sin, - "cos": maingopy.cos, - "tan": maingopy.tan, - "cosh": maingopy.cosh, - "sinh": maingopy.sinh, - "tanh": maingopy.tanh, - "asin": maingopy.asin, - "acos": maingopy.acos, - "atan": maingopy.atan, - "exp": maingopy.exp, - "sqrt": maingopy.sqrt, - "asinh": ToMAiNGOVisitor.maingo_asinh, - "acosh": ToMAiNGOVisitor.maingo_acosh, - "atanh": ToMAiNGOVisitor.maingo_atanh, - } - - @classmethod - def maingo_log10(cls, x): - return maingopy.log(x) / math.log(10) - - @classmethod - def maingo_asinh(cls, x): - return maingopy.log(x + maingopy.sqrt(maingopy.pow(x, 2) + 1)) - - @classmethod - def maingo_acosh(cls, x): - return maingopy.log(x + maingopy.sqrt(maingopy.pow(x, 2) - 1)) - - @classmethod - def maingo_atanh(cls, x): - return 0.5 * maingopy.log(x + 1) - 0.5 * maingopy.log(1 - x) - - def visit(self, node, values): - """Visit nodes that have been expanded""" - for i, val in enumerate(values): - arg = node._args_[i] - - if arg is None: - values[i] = "Undefined" - elif arg.__class__ in native_numeric_types: - pass - elif arg.__class__ in nonpyomo_leaf_types: - values[i] = val - else: - parens = False - if arg.is_expression_type() and node.PRECEDENCE is not None: - if arg.PRECEDENCE is None: - pass - elif node.PRECEDENCE < arg.PRECEDENCE: - parens = True - elif node.PRECEDENCE == arg.PRECEDENCE: - if i == 0: - parens = node.ASSOCIATIVITY != LEFT_TO_RIGHT - elif i == len(node._args_) - 1: - parens = node.ASSOCIATIVITY != RIGHT_TO_LEFT - else: - parens = True - if parens: - values[i] = val - - if node.__class__ in EXPR.NPV_expression_types: - return value(node) - - if node.__class__ in {EXPR.ProductExpression, EXPR.MonomialTermExpression}: - return values[0] * values[1] - - if node.__class__ in {EXPR.SumExpression}: - return sum(values) - - if node.__class__ in {EXPR.PowExpression}: - return maingopy.pow(values[0], values[1]) - - if node.__class__ in {EXPR.DivisionExpression}: - return values[0] / values[1] - - if node.__class__ in {EXPR.NegationExpression}: - return -values[0] - - if node.__class__ in {EXPR.AbsExpression}: - return maingopy.abs(values[0]) - - if node.__class__ in {EXPR.UnaryFunctionExpression}: - pyomo_func = node.getname() - maingo_func = self._pyomo_func_to_maingo_func[pyomo_func] - return maingo_func(values[0]) - - if node.__class__ in {ScalarExpression}: - return values[0] - - raise ValueError(f"Unknown function expression encountered: {node.getname()}") - - def visiting_potential_leaf(self, node): - """ - Visiting a potential leaf. - - Return True if the node is not expanded. - """ - if node.__class__ in native_types: - return True, node - - if node.is_expression_type(): - if node.__class__ is EXPR.MonomialTermExpression: - return True, self._monomial_to_maingo(node) - if node.__class__ is EXPR.LinearExpression: - return True, self._linear_to_maingo(node) - return False, None - - if node.is_component_type(): - if node.ctype not in valid_expr_ctypes_minlp: - # Make sure all components in active constraints - # are basic ctypes we know how to deal with. - raise RuntimeError( - "Unallowable component '%s' of type %s found in an active " - "constraint or objective.\nMAiNGO cannot export " - "expressions with this component type." - % (node.name, node.ctype.__name__) - ) - - if node.is_fixed(): - return True, node() - else: - assert node.is_variable_type() - maingo_var_id = self.idmap[id(node)] - maingo_var = self.variables[maingo_var_id] - return True, maingo_var - - def _monomial_to_maingo(self, node): - const, var = node.args - maingo_var_id = self.idmap[id(var)] - maingo_var = self.variables[maingo_var_id] - if const.__class__ not in native_types: - const = value(const) - if var.is_fixed(): - return const * var.value - if not const: - return 0 - if const in _plusMinusOne: - if const < 0: - return -maingo_var - else: - return maingo_var - return const * maingo_var - - def _linear_to_maingo(self, node): - values = [ - ( - self._monomial_to_maingo(arg) - if ( - arg.__class__ is EXPR.MonomialTermExpression - and not arg.arg(1).is_fixed() - ) - else value(arg) - ) - for arg in node.args - ] - return sum(values) - - class MAiNGO(PersistentBase, PersistentSolver): """ Interface to MAiNGO @@ -536,7 +319,8 @@ def set_instance(self, model): self._labeler = NumericLabeler("x") self.add_block(model) - self._solver_model = SolverModel( + + self._solver_model = maingo_solvermodel.SolverModel( var_list=self._maingo_vars, con_list=self._cons, objective=self._objective, diff --git a/pyomo/contrib/appsi/solvers/maingo_solvermodel.py b/pyomo/contrib/appsi/solvers/maingo_solvermodel.py new file mode 100644 index 00000000000..4abc53ae290 --- /dev/null +++ b/pyomo/contrib/appsi/solvers/maingo_solvermodel.py @@ -0,0 +1,257 @@ +import math + +from pyomo.common.dependencies import attempt_import +from pyomo.core.base.var import ScalarVar +import pyomo.core.expr.expr_common as common +import pyomo.core.expr as EXPR +from pyomo.core.expr.numvalue import ( + value, + is_constant, + is_fixed, + native_numeric_types, + native_types, + nonpyomo_leaf_types, +) +from pyomo.core.kernel.objective import minimize, maximize +from pyomo.repn.util import valid_expr_ctypes_minlp + + +def _import_maingopy(): + try: + import maingopy + except ImportError: + raise + return maingopy + + +maingopy, maingopy_available = attempt_import("maingopy", importer=_import_maingopy) + +_plusMinusOne = {1, -1} + +LEFT_TO_RIGHT = common.OperatorAssociativity.LEFT_TO_RIGHT +RIGHT_TO_LEFT = common.OperatorAssociativity.RIGHT_TO_LEFT + + +class ToMAiNGOVisitor(EXPR.ExpressionValueVisitor): + def __init__(self, variables, idmap): + super(ToMAiNGOVisitor, self).__init__() + self.variables = variables + self.idmap = idmap + self._pyomo_func_to_maingo_func = { + "log": maingopy.log, + "log10": ToMAiNGOVisitor.maingo_log10, + "sin": maingopy.sin, + "cos": maingopy.cos, + "tan": maingopy.tan, + "cosh": maingopy.cosh, + "sinh": maingopy.sinh, + "tanh": maingopy.tanh, + "asin": maingopy.asin, + "acos": maingopy.acos, + "atan": maingopy.atan, + "exp": maingopy.exp, + "sqrt": maingopy.sqrt, + "asinh": ToMAiNGOVisitor.maingo_asinh, + "acosh": ToMAiNGOVisitor.maingo_acosh, + "atanh": ToMAiNGOVisitor.maingo_atanh, + } + + @classmethod + def maingo_log10(cls, x): + return maingopy.log(x) / math.log(10) + + @classmethod + def maingo_asinh(cls, x): + return maingopy.log(x + maingopy.sqrt(maingopy.pow(x, 2) + 1)) + + @classmethod + def maingo_acosh(cls, x): + return maingopy.log(x + maingopy.sqrt(maingopy.pow(x, 2) - 1)) + + @classmethod + def maingo_atanh(cls, x): + return 0.5 * maingopy.log(x + 1) - 0.5 * maingopy.log(1 - x) + + def visit(self, node, values): + """Visit nodes that have been expanded""" + for i, val in enumerate(values): + arg = node._args_[i] + + if arg is None: + values[i] = "Undefined" + elif arg.__class__ in native_numeric_types: + pass + elif arg.__class__ in nonpyomo_leaf_types: + values[i] = val + else: + parens = False + if arg.is_expression_type() and node.PRECEDENCE is not None: + if arg.PRECEDENCE is None: + pass + elif node.PRECEDENCE < arg.PRECEDENCE: + parens = True + elif node.PRECEDENCE == arg.PRECEDENCE: + if i == 0: + parens = node.ASSOCIATIVITY != LEFT_TO_RIGHT + elif i == len(node._args_) - 1: + parens = node.ASSOCIATIVITY != RIGHT_TO_LEFT + else: + parens = True + if parens: + values[i] = val + + if node.__class__ in EXPR.NPV_expression_types: + return value(node) + + if node.__class__ in {EXPR.ProductExpression, EXPR.MonomialTermExpression}: + return values[0] * values[1] + + if node.__class__ in {EXPR.SumExpression}: + return sum(values) + + if node.__class__ in {EXPR.PowExpression}: + return maingopy.pow(values[0], values[1]) + + if node.__class__ in {EXPR.DivisionExpression}: + return values[0] / values[1] + + if node.__class__ in {EXPR.NegationExpression}: + return -values[0] + + if node.__class__ in {EXPR.AbsExpression}: + return maingopy.abs(values[0]) + + if node.__class__ in {EXPR.UnaryFunctionExpression}: + pyomo_func = node.getname() + maingo_func = self._pyomo_func_to_maingo_func[pyomo_func] + return maingo_func(values[0]) + + if node.__class__ in {ScalarExpression}: + return values[0] + + raise ValueError(f"Unknown function expression encountered: {node.getname()}") + + def visiting_potential_leaf(self, node): + """ + Visiting a potential leaf. + + Return True if the node is not expanded. + """ + if node.__class__ in native_types: + return True, node + + if node.is_expression_type(): + if node.__class__ is EXPR.MonomialTermExpression: + return True, self._monomial_to_maingo(node) + if node.__class__ is EXPR.LinearExpression: + return True, self._linear_to_maingo(node) + return False, None + + if node.is_component_type(): + if node.ctype not in valid_expr_ctypes_minlp: + # Make sure all components in active constraints + # are basic ctypes we know how to deal with. + raise RuntimeError( + "Unallowable component '%s' of type %s found in an active " + "constraint or objective.\nMAiNGO cannot export " + "expressions with this component type." + % (node.name, node.ctype.__name__) + ) + + if node.is_fixed(): + return True, node() + else: + assert node.is_variable_type() + maingo_var_id = self.idmap[id(node)] + maingo_var = self.variables[maingo_var_id] + return True, maingo_var + + def _monomial_to_maingo(self, node): + if node.__class__ is ScalarVar: + var = node + const = 1 + else: + const, var = node.args + maingo_var_id = self.idmap[id(var)] + maingo_var = self.variables[maingo_var_id] + if const.__class__ not in native_types: + const = value(const) + if var.is_fixed(): + return const * var.value + if not const: + return 0 + if const in _plusMinusOne: + if const < 0: + return -maingo_var + else: + return maingo_var + return const * maingo_var + + def _linear_to_maingo(self, node): + values = [ + ( + self._monomial_to_maingo(arg) + if (arg.__class__ in {EXPR.MonomialTermExpression, ScalarVar}) + else (value(arg)) + ) + for arg in node.args + ] + return sum(values) + + +class SolverModel(maingopy.MAiNGOmodel): + def __init__(self, var_list, objective, con_list, idmap): + maingopy.MAiNGOmodel.__init__(self) + self._var_list = var_list + self._con_list = con_list + self._objective = objective + self._idmap = idmap + + def build_maingo_objective(self, obj, visitor): + maingo_obj = visitor.dfs_postorder_stack(obj.expr) + if obj.sense == maximize: + maingo_obj *= -1 + return maingo_obj + + def build_maingo_constraints(self, cons, visitor): + eqs = [] + ineqs = [] + for con in cons: + if con.equality: + eqs += [visitor.dfs_postorder_stack(con.body - con.lower)] + elif con.has_ub() and con.has_lb(): + ineqs += [visitor.dfs_postorder_stack(con.body - con.upper)] + ineqs += [visitor.dfs_postorder_stack(con.lower - con.body)] + elif con.has_ub(): + ineqs += [visitor.dfs_postorder_stack(con.body - con.upper)] + elif con.has_ub(): + ineqs += [visitor.dfs_postorder_stack(con.lower - con.body)] + else: + raise ValueError( + "Constraint does not have a lower " + "or an upper bound: {0} \n".format(con) + ) + return eqs, ineqs + + def get_variables(self): + return [ + maingopy.OptimizationVariable( + maingopy.Bounds(var.lb, var.ub), var.type, var.name + ) + for var in self._var_list + ] + + def get_initial_point(self): + return [ + var.init if not var.init is None else (var.lb + var.ub) / 2.0 + for var in self._var_list + ] + + def evaluate(self, maingo_vars): + visitor = ToMAiNGOVisitor(maingo_vars, self._idmap) + result = maingopy.EvaluationContainer() + result.objective = self.build_maingo_objective(self._objective, visitor) + eqs, ineqs = self.build_maingo_constraints(self._con_list, visitor) + result.eq = eqs + result.ineq = ineqs + return result From 6e182bed965e98ead3578c1a2d79244c93760e00 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Tue, 2 Apr 2024 11:29:38 +0200 Subject: [PATCH 14/39] Add MAiNGO to test_perisistent_solvers.py --- .../solvers/tests/test_persistent_solvers.py | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index ae189aca701..c063adc2bfe 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -17,7 +17,7 @@ parameterized = parameterized.parameterized from pyomo.contrib.appsi.base import TerminationCondition, Results, PersistentSolver from pyomo.contrib.appsi.cmodel import cmodel_available -from pyomo.contrib.appsi.solvers import Gurobi, Ipopt, Cplex, Cbc, Highs +from pyomo.contrib.appsi.solvers import Gurobi, Ipopt, Cplex, Cbc, Highs, MAiNGO from typing import Type from pyomo.core.expr.numeric_expr import LinearExpression import os @@ -36,11 +36,23 @@ ('cplex', Cplex), ('cbc', Cbc), ('highs', Highs), + ('maingo', MAiNGO), ] -mip_solvers = [('gurobi', Gurobi), ('cplex', Cplex), ('cbc', Cbc), ('highs', Highs)] -nlp_solvers = [('ipopt', Ipopt)] -qcp_solvers = [('gurobi', Gurobi), ('ipopt', Ipopt), ('cplex', Cplex)] -miqcqp_solvers = [('gurobi', Gurobi), ('cplex', Cplex)] +mip_solvers = [ + ('gurobi', Gurobi), + ('cplex', Cplex), + ('cbc', Cbc), + ('highs', Highs), + ('maingo', MAiNGO), +] +nlp_solvers = [('ipopt', Ipopt), ('maingo', MAiNGO)] +qcp_solvers = [ + ('gurobi', Gurobi), + ('ipopt', Ipopt), + ('cplex', Cplex), + ('maingo', MAiNGO), +] +miqcqp_solvers = [('gurobi', Gurobi), ('cplex', Cplex), ('maingo', MAiNGO)] only_child_vars_options = [True, False] From 76ba0eb525b5a2037220b84e17edde93715cb599 Mon Sep 17 00:00:00 2001 From: MAiNGO-github <139969768+MAiNGO-github@users.noreply.github.com> Date: Wed, 17 Apr 2024 09:29:14 +0200 Subject: [PATCH 15/39] Update pyomo/contrib/appsi/solvers/maingo.py Co-authored-by: Miranda Mundt <55767766+mrmundt@users.noreply.github.com> --- pyomo/contrib/appsi/solvers/maingo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/appsi/solvers/maingo.py b/pyomo/contrib/appsi/solvers/maingo.py index 29464e6a876..017841d1b8c 100644 --- a/pyomo/contrib/appsi/solvers/maingo.py +++ b/pyomo/contrib/appsi/solvers/maingo.py @@ -384,7 +384,7 @@ def _postsolve(self, timer: HierarchicalTimer): results.termination_condition = TerminationCondition.optimal if status == maingopy.FEASIBLE_POINT: logger.warning( - "MAiNGO did only find a feasible solution but did not prove its global optimality." + "MAiNGO found a feasible solution but did not prove its global optimality." ) elif status == maingopy.INFEASIBLE: results.termination_condition = TerminationCondition.infeasible From f9ada2946d295da4d53e56c3a7cc2f78e5bbaa1e Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Wed, 17 Apr 2024 15:47:11 +0200 Subject: [PATCH 16/39] Restrict y to positive values (MAiNGO cannot handle 1/0) --- pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index c063adc2bfe..58806d1e86c 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -688,7 +688,7 @@ def test_fixed_vars_4( raise unittest.SkipTest m = pe.ConcreteModel() m.x = pe.Var() - m.y = pe.Var() + m.y = pe.Var(bounds=(1e-6, None)) m.obj = pe.Objective(expr=m.x**2 + m.y**2) m.c1 = pe.Constraint(expr=m.x == 2 / m.y) m.y.fix(1) From 42fcd17429cc2b608cc26544dc19c8cd3074bbb9 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Wed, 17 Apr 2024 15:48:15 +0200 Subject: [PATCH 17/39] Restrict y to positive values (MAiNGO cannot handle log(negative)) --- pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index 58806d1e86c..5a46c1d3e5b 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -858,7 +858,7 @@ def test_log(self, name: str, opt_class: Type[PersistentSolver], only_child_vars if not opt.available(): raise unittest.SkipTest m = pe.ConcreteModel() - m.x = pe.Var(initialize=1) + m.x = pe.Var(initialize=1, bounds=(1e-6, None)) m.y = pe.Var() m.obj = pe.Objective(expr=m.x**2 + m.y**2) m.c1 = pe.Constraint(expr=m.y <= pe.log(m.x)) From bec9be7e9e3f4c5d221ddb076ddcf7697616182b Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Wed, 17 Apr 2024 15:49:48 +0200 Subject: [PATCH 18/39] Exluded MAiNGO for checking unbounded termination criterion --- .../solvers/tests/test_persistent_solvers.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index 5a46c1d3e5b..660ba60f26f 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -1090,13 +1090,14 @@ def test_objective_changes( m.obj.sense = pe.maximize opt.config.load_solution = False res = opt.solve(m) - self.assertIn( - res.termination_condition, - { - TerminationCondition.unbounded, - TerminationCondition.infeasibleOrUnbounded, - }, - ) + if not isinstance(opt, MAiNGO): + self.assertIn( + res.termination_condition, + { + TerminationCondition.unbounded, + TerminationCondition.infeasibleOrUnbounded, + }, + ) m.obj.sense = pe.minimize opt.config.load_solution = True m.obj = pe.Objective(expr=m.x * m.y) From 6db9970a41760b64cf5e43ddc73eb3f8eb0aefc0 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Wed, 17 Apr 2024 15:54:44 +0200 Subject: [PATCH 19/39] Create MAiNGOTest class with tighter tolerances to pass tests --- pyomo/contrib/appsi/solvers/maingo.py | 16 ++++++++++++++++ .../solvers/tests/test_persistent_solvers.py | 3 ++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/appsi/solvers/maingo.py b/pyomo/contrib/appsi/solvers/maingo.py index 29464e6a876..bff8d6f594e 100644 --- a/pyomo/contrib/appsi/solvers/maingo.py +++ b/pyomo/contrib/appsi/solvers/maingo.py @@ -201,6 +201,9 @@ def _solve(self, timer: HierarchicalTimer): self._mymaingo.set_option("loggingDestination", 2) self._mymaingo.set_log_file_name(config.logfile) + self._mymaingo.set_option("epsilonA", 1e-4) + self._mymaingo.set_option("epsilonR", 1e-4) + self._set_maingo_options() if config.time_limit is not None: self._mymaingo.set_option("maxTime", config.time_limit) @@ -480,3 +483,16 @@ def get_reduced_costs(self, vars_to_load=None): def get_duals(self, cons_to_load=None): raise ValueError("MAiNGO does not support returning Duals") + + + def _set_maingo_options(self): + pass + + +# Solver class with tighter tolerances for testing +class MAiNGOTest(MAiNGO): + def _set_maingo_options(self): + self._mymaingo.set_option("epsilonA", 1e-8) + self._mymaingo.set_option("epsilonR", 1e-8) + self._mymaingo.set_option("deltaIneq", 1e-9) + self._mymaingo.set_option("deltaEq", 1e-9) diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index 660ba60f26f..23440065491 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -17,7 +17,8 @@ parameterized = parameterized.parameterized from pyomo.contrib.appsi.base import TerminationCondition, Results, PersistentSolver from pyomo.contrib.appsi.cmodel import cmodel_available -from pyomo.contrib.appsi.solvers import Gurobi, Ipopt, Cplex, Cbc, Highs, MAiNGO +from pyomo.contrib.appsi.solvers import Gurobi, Ipopt, Cplex, Cbc, Highs +from pyomo.contrib.appsi.solvers import MAiNGOTest as MAiNGO from typing import Type from pyomo.core.expr.numeric_expr import LinearExpression import os From de7fc5c68360bb1c49d351eca21ffff3d7cf4d60 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Wed, 17 Apr 2024 15:56:01 +0200 Subject: [PATCH 20/39] Exclude duals and RCs for tests with MAiNGO --- .../solvers/tests/test_persistent_solvers.py | 176 ++++++++++-------- 1 file changed, 99 insertions(+), 77 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index 23440065491..f50461af373 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -185,14 +185,16 @@ def test_range_constraint( res = opt.solve(m) self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, -1) - duals = opt.get_duals() - self.assertAlmostEqual(duals[m.c], 1) + if not opt_class is MAiNGO: + duals = opt.get_duals() + self.assertAlmostEqual(duals[m.c], 1) m.obj.sense = pe.maximize res = opt.solve(m) self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, 1) - duals = opt.get_duals() - self.assertAlmostEqual(duals[m.c], 1) + if not opt_class is MAiNGO: + duals = opt.get_duals() + self.assertAlmostEqual(duals[m.c], 1) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_reduced_costs( @@ -209,9 +211,10 @@ def test_reduced_costs( self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, -1) self.assertAlmostEqual(m.y.value, -2) - rc = opt.get_reduced_costs() - self.assertAlmostEqual(rc[m.x], 3) - self.assertAlmostEqual(rc[m.y], 4) + if not opt_class is MAiNGO: + rc = opt.get_reduced_costs() + self.assertAlmostEqual(rc[m.x], 3) + self.assertAlmostEqual(rc[m.y], 4) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_reduced_costs2( @@ -226,14 +229,16 @@ def test_reduced_costs2( res = opt.solve(m) self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, -1) - rc = opt.get_reduced_costs() - self.assertAlmostEqual(rc[m.x], 1) + if not opt_class is MAiNGO: + rc = opt.get_reduced_costs() + self.assertAlmostEqual(rc[m.x], 1) m.obj.sense = pe.maximize res = opt.solve(m) self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, 1) - rc = opt.get_reduced_costs() - self.assertAlmostEqual(rc[m.x], 1) + if not opt_class is MAiNGO: + rc = opt.get_reduced_costs() + self.assertAlmostEqual(rc[m.x], 1) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_param_changes( @@ -265,9 +270,10 @@ def test_param_changes( self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) self.assertTrue(res.best_objective_bound <= m.y.value) - duals = opt.get_duals() - self.assertAlmostEqual(duals[m.c1], (1 + a1 / (a2 - a1))) - self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) + if not opt_class is MAiNGO: + duals = opt.get_duals() + self.assertAlmostEqual(duals[m.c1], (1 + a1 / (a2 - a1))) + self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_immutable_param( @@ -303,9 +309,10 @@ def test_immutable_param( self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) self.assertTrue(res.best_objective_bound <= m.y.value) - duals = opt.get_duals() - self.assertAlmostEqual(duals[m.c1], (1 + a1 / (a2 - a1))) - self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) + if not opt_class is MAiNGO: + duals = opt.get_duals() + self.assertAlmostEqual(duals[m.c1], (1 + a1 / (a2 - a1))) + self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_equality( @@ -337,9 +344,10 @@ def test_equality( self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) self.assertTrue(res.best_objective_bound <= m.y.value) - duals = opt.get_duals() - self.assertAlmostEqual(duals[m.c1], (1 + a1 / (a2 - a1))) - self.assertAlmostEqual(duals[m.c2], -a1 / (a2 - a1)) + if not opt_class is MAiNGO: + duals = opt.get_duals() + self.assertAlmostEqual(duals[m.c1], (1 + a1 / (a2 - a1))) + self.assertAlmostEqual(duals[m.c2], -a1 / (a2 - a1)) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_linear_expression( @@ -407,9 +415,10 @@ def test_no_objective( self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertEqual(res.best_feasible_objective, None) self.assertEqual(res.best_objective_bound, None) - duals = opt.get_duals() - self.assertAlmostEqual(duals[m.c1], 0) - self.assertAlmostEqual(duals[m.c2], 0) + if not opt_class is MAiNGO: + duals = opt.get_duals() + self.assertAlmostEqual(duals[m.c1], 0) + self.assertAlmostEqual(duals[m.c2], 0) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_add_remove_cons( @@ -436,9 +445,10 @@ def test_add_remove_cons( self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) self.assertTrue(res.best_objective_bound <= m.y.value) - duals = opt.get_duals() - self.assertAlmostEqual(duals[m.c1], -(1 + a1 / (a2 - a1))) - self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) + if not opt_class is MAiNGO: + duals = opt.get_duals() + self.assertAlmostEqual(duals[m.c1], -(1 + a1 / (a2 - a1))) + self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) m.c3 = pe.Constraint(expr=m.y >= a3 * m.x + b3) res = opt.solve(m) @@ -447,10 +457,11 @@ def test_add_remove_cons( self.assertAlmostEqual(m.y.value, a1 * (b3 - b1) / (a1 - a3) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) self.assertTrue(res.best_objective_bound <= m.y.value) - duals = opt.get_duals() - self.assertAlmostEqual(duals[m.c1], -(1 + a1 / (a3 - a1))) - self.assertAlmostEqual(duals[m.c2], 0) - self.assertAlmostEqual(duals[m.c3], a1 / (a3 - a1)) + if not opt_class is MAiNGO: + duals = opt.get_duals() + self.assertAlmostEqual(duals[m.c1], -(1 + a1 / (a3 - a1))) + self.assertAlmostEqual(duals[m.c2], 0) + self.assertAlmostEqual(duals[m.c3], a1 / (a3 - a1)) del m.c3 res = opt.solve(m) @@ -459,9 +470,10 @@ def test_add_remove_cons( self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) self.assertTrue(res.best_objective_bound <= m.y.value) - duals = opt.get_duals() - self.assertAlmostEqual(duals[m.c1], -(1 + a1 / (a2 - a1))) - self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) + if not opt_class is MAiNGO: + duals = opt.get_duals() + self.assertAlmostEqual(duals[m.c1], -(1 + a1 / (a2 - a1))) + self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_results_infeasible( @@ -500,14 +512,15 @@ def test_results_infeasible( RuntimeError, '.*does not currently have a valid solution.*' ): res.solution_loader.load_vars() - with self.assertRaisesRegex( - RuntimeError, '.*does not currently have valid duals.*' - ): - res.solution_loader.get_duals() - with self.assertRaisesRegex( - RuntimeError, '.*does not currently have valid reduced costs.*' - ): - res.solution_loader.get_reduced_costs() + if not opt_class is MAiNGO: + with self.assertRaisesRegex( + RuntimeError, '.*does not currently have valid duals.*' + ): + res.solution_loader.get_duals() + with self.assertRaisesRegex( + RuntimeError, '.*does not currently have valid reduced costs.*' + ): + res.solution_loader.get_reduced_costs() @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_duals(self, name: str, opt_class: Type[PersistentSolver], only_child_vars): @@ -524,13 +537,14 @@ def test_duals(self, name: str, opt_class: Type[PersistentSolver], only_child_va res = opt.solve(m) self.assertAlmostEqual(m.x.value, 1) self.assertAlmostEqual(m.y.value, 1) - duals = opt.get_duals() - self.assertAlmostEqual(duals[m.c1], 0.5) - self.assertAlmostEqual(duals[m.c2], 0.5) + if not opt_class is MAiNGO: + duals = opt.get_duals() + self.assertAlmostEqual(duals[m.c1], 0.5) + self.assertAlmostEqual(duals[m.c2], 0.5) - duals = opt.get_duals(cons_to_load=[m.c1]) - self.assertAlmostEqual(duals[m.c1], 0.5) - self.assertNotIn(m.c2, duals) + duals = opt.get_duals(cons_to_load=[m.c1]) + self.assertAlmostEqual(duals[m.c1], 0.5) + self.assertNotIn(m.c2, duals) @parameterized.expand(input=_load_tests(qcp_solvers, only_child_vars_options)) def test_mutable_quadratic_coefficient( @@ -778,17 +792,19 @@ def test_mutable_param_with_range( self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1, 6) self.assertAlmostEqual(res.best_feasible_objective, m.y.value, 6) self.assertTrue(res.best_objective_bound <= m.y.value + 1e-12) - duals = opt.get_duals() - self.assertAlmostEqual(duals[m.con1], (1 + a1 / (a2 - a1)), 6) - self.assertAlmostEqual(duals[m.con2], -a1 / (a2 - a1), 6) + if not opt_class is MAiNGO: + duals = opt.get_duals() + self.assertAlmostEqual(duals[m.con1], (1 + a1 / (a2 - a1)), 6) + self.assertAlmostEqual(duals[m.con2], -a1 / (a2 - a1), 6) else: self.assertAlmostEqual(m.x.value, (c2 - c1) / (a1 - a2), 6) self.assertAlmostEqual(m.y.value, a1 * (c2 - c1) / (a1 - a2) + c1, 6) self.assertAlmostEqual(res.best_feasible_objective, m.y.value, 6) self.assertTrue(res.best_objective_bound >= m.y.value - 1e-12) - duals = opt.get_duals() - self.assertAlmostEqual(duals[m.con1], (1 + a1 / (a2 - a1)), 6) - self.assertAlmostEqual(duals[m.con2], -a1 / (a2 - a1), 6) + if not opt_class is MAiNGO: + duals = opt.get_duals() + self.assertAlmostEqual(duals[m.con1], (1 + a1 / (a2 - a1)), 6) + self.assertAlmostEqual(duals[m.con2], -a1 / (a2 - a1), 6) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_add_and_remove_vars( @@ -986,24 +1002,25 @@ def test_solution_loader( self.assertNotIn(m.x, primals) self.assertIn(m.y, primals) self.assertAlmostEqual(primals[m.y], 1) - reduced_costs = res.solution_loader.get_reduced_costs() - self.assertIn(m.x, reduced_costs) - self.assertIn(m.y, reduced_costs) - self.assertAlmostEqual(reduced_costs[m.x], 1) - self.assertAlmostEqual(reduced_costs[m.y], 0) - reduced_costs = res.solution_loader.get_reduced_costs([m.y]) - self.assertNotIn(m.x, reduced_costs) - self.assertIn(m.y, reduced_costs) - self.assertAlmostEqual(reduced_costs[m.y], 0) - duals = res.solution_loader.get_duals() - self.assertIn(m.c1, duals) - self.assertIn(m.c2, duals) - self.assertAlmostEqual(duals[m.c1], 1) - self.assertAlmostEqual(duals[m.c2], 0) - duals = res.solution_loader.get_duals([m.c1]) - self.assertNotIn(m.c2, duals) - self.assertIn(m.c1, duals) - self.assertAlmostEqual(duals[m.c1], 1) + if not opt_class is MAiNGO: + reduced_costs = res.solution_loader.get_reduced_costs() + self.assertIn(m.x, reduced_costs) + self.assertIn(m.y, reduced_costs) + self.assertAlmostEqual(reduced_costs[m.x], 1) + self.assertAlmostEqual(reduced_costs[m.y], 0) + reduced_costs = res.solution_loader.get_reduced_costs([m.y]) + self.assertNotIn(m.x, reduced_costs) + self.assertIn(m.y, reduced_costs) + self.assertAlmostEqual(reduced_costs[m.y], 0) + duals = res.solution_loader.get_duals() + self.assertIn(m.c1, duals) + self.assertIn(m.c2, duals) + self.assertAlmostEqual(duals[m.c1], 1) + self.assertAlmostEqual(duals[m.c2], 0) + duals = res.solution_loader.get_duals([m.c1]) + self.assertNotIn(m.c2, duals) + self.assertIn(m.c1, duals) + self.assertAlmostEqual(duals[m.c1], 1) @parameterized.expand(input=_load_tests(all_solvers, only_child_vars_options)) def test_time_limit( @@ -1373,7 +1390,8 @@ def test_param_updates(self, name: str, opt_class: Type[PersistentSolver]): m.obj = pe.Objective(expr=m.y) m.c1 = pe.Constraint(expr=(0, m.y - m.a1 * m.x - m.b1, None)) m.c2 = pe.Constraint(expr=(None, -m.y + m.a2 * m.x + m.b2, 0)) - m.dual = pe.Suffix(direction=pe.Suffix.IMPORT) + if not opt_class is MAiNGO: + m.dual = pe.Suffix(direction=pe.Suffix.IMPORT) params_to_test = [(1, -1, 2, 1), (1, -2, 2, 1), (1, -1, 3, 1)] for a1, a2, b1, b2 in params_to_test: @@ -1385,8 +1403,9 @@ def test_param_updates(self, name: str, opt_class: Type[PersistentSolver]): pe.assert_optimal_termination(res) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) - self.assertAlmostEqual(m.dual[m.c1], (1 + a1 / (a2 - a1))) - self.assertAlmostEqual(m.dual[m.c2], a1 / (a2 - a1)) + if not opt_class is MAiNGO: + self.assertAlmostEqual(m.dual[m.c1], (1 + a1 / (a2 - a1))) + self.assertAlmostEqual(m.dual[m.c2], a1 / (a2 - a1)) @parameterized.expand(input=all_solvers) def test_load_solutions(self, name: str, opt_class: Type[PersistentSolver]): @@ -1397,11 +1416,14 @@ def test_load_solutions(self, name: str, opt_class: Type[PersistentSolver]): m.x = pe.Var() m.obj = pe.Objective(expr=m.x) m.c = pe.Constraint(expr=(-1, m.x, 1)) - m.dual = pe.Suffix(direction=pe.Suffix.IMPORT) + if not opt_class is MAiNGO: + m.dual = pe.Suffix(direction=pe.Suffix.IMPORT) res = opt.solve(m, load_solutions=False) pe.assert_optimal_termination(res) self.assertIsNone(m.x.value) - self.assertNotIn(m.c, m.dual) + if not opt_class is MAiNGO: + self.assertNotIn(m.c, m.dual) m.solutions.load_from(res) self.assertAlmostEqual(m.x.value, -1) - self.assertAlmostEqual(m.dual[m.c], 1) + if not opt_class is MAiNGO: + self.assertAlmostEqual(m.dual[m.c], 1) From 334b06762bbae40d954ae0ca54720f4f6c448893 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Wed, 17 Apr 2024 15:56:31 +0200 Subject: [PATCH 21/39] MAiNGOTest class in __init__ --- pyomo/contrib/appsi/solvers/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/appsi/solvers/__init__.py b/pyomo/contrib/appsi/solvers/__init__.py index 352571b98f8..c1ebdf28780 100644 --- a/pyomo/contrib/appsi/solvers/__init__.py +++ b/pyomo/contrib/appsi/solvers/__init__.py @@ -15,4 +15,4 @@ from .cplex import Cplex from .highs import Highs from .wntr import Wntr, WntrResults -from .maingo import MAiNGO +from .maingo import MAiNGO, MAiNGOTest From a1e6b92c7518027f8234069acedc9dac86d8b04a Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Wed, 17 Apr 2024 15:57:07 +0200 Subject: [PATCH 22/39] Add copyright statement --- pyomo/contrib/appsi/solvers/maingo.py | 11 +++++++++++ pyomo/contrib/appsi/solvers/maingo_solvermodel.py | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/pyomo/contrib/appsi/solvers/maingo.py b/pyomo/contrib/appsi/solvers/maingo.py index bff8d6f594e..d48e9874712 100644 --- a/pyomo/contrib/appsi/solvers/maingo.py +++ b/pyomo/contrib/appsi/solvers/maingo.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + from collections import namedtuple import logging import math diff --git a/pyomo/contrib/appsi/solvers/maingo_solvermodel.py b/pyomo/contrib/appsi/solvers/maingo_solvermodel.py index 4abc53ae290..686d7c54657 100644 --- a/pyomo/contrib/appsi/solvers/maingo_solvermodel.py +++ b/pyomo/contrib/appsi/solvers/maingo_solvermodel.py @@ -1,3 +1,14 @@ +# ___________________________________________________________________________ +# +# Pyomo: Python Optimization Modeling Objects +# Copyright (c) 2008-2024 +# National Technology and Engineering Solutions of Sandia, LLC +# Under the terms of Contract DE-NA0003525 with National Technology and +# Engineering Solutions of Sandia, LLC, the U.S. Government retains certain +# rights in this software. +# This software is distributed under the 3-clause BSD License. +# ___________________________________________________________________________ + import math from pyomo.common.dependencies import attempt_import From 1fab90b84754bd652263a8eed467f081d0de603a Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Wed, 17 Apr 2024 15:58:03 +0200 Subject: [PATCH 23/39] Set default for ConfigValues --- pyomo/contrib/appsi/solvers/maingo.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/maingo.py b/pyomo/contrib/appsi/solvers/maingo.py index d48e9874712..ee85d4549a5 100644 --- a/pyomo/contrib/appsi/solvers/maingo.py +++ b/pyomo/contrib/appsi/solvers/maingo.py @@ -98,13 +98,11 @@ def __init__( visibility=visibility, ) - self.declare("logfile", ConfigValue(domain=str)) - self.declare("solver_output_logger", ConfigValue()) - self.declare("log_level", ConfigValue(domain=NonNegativeInt)) - - self.logfile = "" - self.solver_output_logger = logger - self.log_level = logging.INFO + self.declare("logfile", ConfigValue(domain=str, default="")) + self.declare("solver_output_logger", ConfigValue(default=logger)) + self.declare( + "log_level", ConfigValue(domain=NonNegativeInt, default=logging.INFO) + ) class MAiNGOSolutionLoader(PersistentSolutionLoader): From 5b70135f03575af6c1ef6ddce6bd98c12846194e Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Wed, 17 Apr 2024 15:58:30 +0200 Subject: [PATCH 24/39] Remove check for Python version --- pyomo/contrib/appsi/solvers/maingo.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/maingo.py b/pyomo/contrib/appsi/solvers/maingo.py index ee85d4549a5..0886ce21c75 100644 --- a/pyomo/contrib/appsi/solvers/maingo.py +++ b/pyomo/contrib/appsi/solvers/maingo.py @@ -151,15 +151,9 @@ def available(self): return self._available def version(self): - # Check if Python >= 3.8 - if sys.version_info.major >= 3 and sys.version_info.minor >= 8: - from importlib.metadata import version + import pkg_resources - version = version('maingopy') - else: - import pkg_resources - - version = pkg_resources.get_distribution('maingopy').version + version = pkg_resources.get_distribution('maingopy').version return tuple(int(k) for k in version.split('.')) From 55b7dff375cc9c82cd2d425698c521090711a816 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Wed, 17 Apr 2024 16:00:41 +0200 Subject: [PATCH 25/39] Add missing functionalities and fix bugs --- pyomo/contrib/appsi/solvers/maingo.py | 81 ++++++++++++++----- .../appsi/solvers/maingo_solvermodel.py | 51 ++++++++---- 2 files changed, 97 insertions(+), 35 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/maingo.py b/pyomo/contrib/appsi/solvers/maingo.py index 0886ce21c75..eabfdd36267 100644 --- a/pyomo/contrib/appsi/solvers/maingo.py +++ b/pyomo/contrib/appsi/solvers/maingo.py @@ -120,6 +120,7 @@ def __init__(self, solver): super(MAiNGOResults, self).__init__() self.wallclock_time = None self.cpu_time = None + self.globally_optimal = None self.solution_loader = MAiNGOSolutionLoader(solver=solver) @@ -228,9 +229,14 @@ def solve(self, model, timer: HierarchicalTimer = None): self._last_results_object.solution_loader.invalidate() if timer is None: timer = HierarchicalTimer() - timer.start("set_instance") - self.set_instance(model) - timer.stop("set_instance") + if model is not self._model: + timer.start("set_instance") + self.set_instance(model) + timer.stop("set_instance") + else: + timer.start("Update") + self.update(timer=timer) + timer.stop("Update") res = self._solve(timer) self._last_results_object = res if self.config.report_timing: @@ -285,7 +291,7 @@ def _process_domain_and_bounds(self, var): return lb, ub, vtype def _add_variables(self, variables: List[_GeneralVarData]): - for ndx, var in enumerate(variables): + for var in variables: varname = self._symbol_map.getSymbol(var, self._labeler) lb, ub, vtype = self._process_domain_and_bounds(var) self._maingo_vars.append( @@ -331,10 +337,11 @@ def set_instance(self, model): con_list=self._cons, objective=self._objective, idmap=self._pyomo_var_to_solver_var_id_map, + logger=logger, ) def _add_constraints(self, cons: List[_GeneralConstraintData]): - self._cons = cons + self._cons += cons def _add_sos_constraints(self, cons: List[_SOSConstraintData]): if len(cons) >= 1: @@ -344,7 +351,8 @@ def _add_sos_constraints(self, cons: List[_SOSConstraintData]): pass def _remove_constraints(self, cons: List[_GeneralConstraintData]): - pass + for con in cons: + self._cons.remove(con) def _remove_sos_constraints(self, cons: List[_SOSConstraintData]): if len(cons) >= 1: @@ -354,28 +362,48 @@ def _remove_sos_constraints(self, cons: List[_SOSConstraintData]): pass def _remove_variables(self, variables: List[_GeneralVarData]): - pass + removed_maingo_vars = [] + for var in variables: + varname = self._symbol_map.getSymbol(var, self._labeler) + del self._maingo_vars[self._pyomo_var_to_solver_var_id_map[id(var)]] + removed_maingo_vars += [self._pyomo_var_to_solver_var_id_map[id(var)]] + del self._pyomo_var_to_solver_var_id_map[id(var)] + + for pyomo_var, maingo_var_id in self._pyomo_var_to_solver_var_id_map.items(): + # How many variables before current var where removed? + num_removed = 0 + for removed_var in removed_maingo_vars: + if removed_var <= maingo_var_id: + num_removed += 1 + self._pyomo_var_to_solver_var_id_map[pyomo_var] = ( + maingo_var_id - num_removed + ) def _remove_params(self, params: List[_ParamData]): pass def _update_variables(self, variables: List[_GeneralVarData]): - pass + for var in variables: + if id(var) not in self._pyomo_var_to_solver_var_id_map: + raise ValueError( + 'The Var provided to update_var needs to be added first: {0}'.format( + var + ) + ) + lb, ub, vtype = self._process_domain_and_bounds(var) + self._maingo_vars[self._pyomo_var_to_solver_var_id_map[id(var)]] = ( + MaingoVar(name=var.name, type=vtype, lb=lb, ub=ub, init=var.value) + ) def update_params(self): - pass + vars = [var[0] for var in self._vars.values()] + self._update_variables(vars) def _set_objective(self, obj): - if obj is None: - raise NotImplementedError( - "MAiNGO needs a objective. Please set a dummy objective." - ) - else: - if not obj.sense in {minimize, maximize}: - raise ValueError( - "Objective sense is not recognized: {0}".format(obj.sense) - ) - self._objective = obj + + if not obj.sense in {minimize, maximize}: + raise ValueError("Objective sense is not recognized: {0}".format(obj.sense)) + self._objective = obj def _postsolve(self, timer: HierarchicalTimer): config = self.config @@ -388,7 +416,9 @@ def _postsolve(self, timer: HierarchicalTimer): if status in {maingopy.GLOBALLY_OPTIMAL, maingopy.FEASIBLE_POINT}: results.termination_condition = TerminationCondition.optimal + results.globally_optimal = True if status == maingopy.FEASIBLE_POINT: + results.globally_optimal = False logger.warning( "MAiNGO did only find a feasible solution but did not prove its global optimality." ) @@ -425,8 +455,8 @@ def _postsolve(self, timer: HierarchicalTimer): timer.start("load solution") if config.load_solution: - if not results.best_feasible_objective is None: - if results.termination_condition != TerminationCondition.optimal: + if results.termination_condition is TerminationCondition.optimal: + if not results.globally_optimal: logger.warning( "Loading a feasible but suboptimal solution. " "Please set load_solution=False and check " @@ -487,6 +517,15 @@ def get_reduced_costs(self, vars_to_load=None): def get_duals(self, cons_to_load=None): raise ValueError("MAiNGO does not support returning Duals") + def update(self, timer: HierarchicalTimer = None): + super(MAiNGO, self).update(timer=timer) + self._solver_model = maingo_solvermodel.SolverModel( + var_list=self._maingo_vars, + con_list=self._cons, + objective=self._objective, + idmap=self._pyomo_var_to_solver_var_id_map, + logger=logger, + ) def _set_maingo_options(self): pass diff --git a/pyomo/contrib/appsi/solvers/maingo_solvermodel.py b/pyomo/contrib/appsi/solvers/maingo_solvermodel.py index 686d7c54657..ca746c4a9b7 100644 --- a/pyomo/contrib/appsi/solvers/maingo_solvermodel.py +++ b/pyomo/contrib/appsi/solvers/maingo_solvermodel.py @@ -13,6 +13,7 @@ from pyomo.common.dependencies import attempt_import from pyomo.core.base.var import ScalarVar +from pyomo.core.base.expression import ScalarExpression import pyomo.core.expr.expr_common as common import pyomo.core.expr as EXPR from pyomo.core.expr.numvalue import ( @@ -178,19 +179,14 @@ def visiting_potential_leaf(self, node): return True, maingo_var def _monomial_to_maingo(self, node): - if node.__class__ is ScalarVar: - var = node - const = 1 - else: - const, var = node.args - maingo_var_id = self.idmap[id(var)] - maingo_var = self.variables[maingo_var_id] + const, var = node.args if const.__class__ not in native_types: const = value(const) if var.is_fixed(): return const * var.value if not const: return 0 + maingo_var = self._var_to_maingo(var) if const in _plusMinusOne: if const < 0: return -maingo_var @@ -198,12 +194,25 @@ def _monomial_to_maingo(self, node): return maingo_var return const * maingo_var + def _var_to_maingo(self, var): + maingo_var_id = self.idmap[id(var)] + maingo_var = self.variables[maingo_var_id] + return maingo_var + def _linear_to_maingo(self, node): values = [ ( self._monomial_to_maingo(arg) - if (arg.__class__ in {EXPR.MonomialTermExpression, ScalarVar}) - else (value(arg)) + if (arg.__class__ is EXPR.MonomialTermExpression) + else ( + value(arg) + if arg.__class__ in native_numeric_types + else ( + self._var_to_maingo(arg) + if arg.is_variable_type() + else value(arg) + ) + ) ) for arg in node.args ] @@ -211,17 +220,25 @@ def _linear_to_maingo(self, node): class SolverModel(maingopy.MAiNGOmodel): - def __init__(self, var_list, objective, con_list, idmap): + def __init__(self, var_list, objective, con_list, idmap, logger): maingopy.MAiNGOmodel.__init__(self) self._var_list = var_list self._con_list = con_list self._objective = objective self._idmap = idmap + self._logger = logger + self._no_objective = False + + if self._objective is None: + self._logger.warning("No objective given, setting a dummy objective of 1.") + self._no_objective = True def build_maingo_objective(self, obj, visitor): + if self._no_objective: + return visitor.variables[-1] maingo_obj = visitor.dfs_postorder_stack(obj.expr) if obj.sense == maximize: - maingo_obj *= -1 + return -1 * maingo_obj return maingo_obj def build_maingo_constraints(self, cons, visitor): @@ -235,7 +252,7 @@ def build_maingo_constraints(self, cons, visitor): ineqs += [visitor.dfs_postorder_stack(con.lower - con.body)] elif con.has_ub(): ineqs += [visitor.dfs_postorder_stack(con.body - con.upper)] - elif con.has_ub(): + elif con.has_lb(): ineqs += [visitor.dfs_postorder_stack(con.lower - con.body)] else: raise ValueError( @@ -245,18 +262,24 @@ def build_maingo_constraints(self, cons, visitor): return eqs, ineqs def get_variables(self): - return [ + vars = [ maingopy.OptimizationVariable( maingopy.Bounds(var.lb, var.ub), var.type, var.name ) for var in self._var_list ] + if self._no_objective: + vars += [maingopy.OptimizationVariable(maingopy.Bounds(1, 1), "dummy_obj")] + return vars def get_initial_point(self): - return [ + initial = [ var.init if not var.init is None else (var.lb + var.ub) / 2.0 for var in self._var_list ] + if self._no_objective: + initial += [1] + return initial def evaluate(self, maingo_vars): visitor = ToMAiNGOVisitor(maingo_vars, self._idmap) From 380f32cb5b1a044002c7c93f5fac394c5ca9c3e2 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Thu, 18 Apr 2024 09:28:54 +0200 Subject: [PATCH 26/39] Register MAiNGO in SolverFactory --- pyomo/contrib/appsi/plugins.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/appsi/plugins.py b/pyomo/contrib/appsi/plugins.py index fbe81484eba..3e1b639ce3b 100644 --- a/pyomo/contrib/appsi/plugins.py +++ b/pyomo/contrib/appsi/plugins.py @@ -11,7 +11,7 @@ from pyomo.common.extensions import ExtensionBuilderFactory from .base import SolverFactory -from .solvers import Gurobi, Ipopt, Cbc, Cplex, Highs +from .solvers import Gurobi, Ipopt, Cbc, Cplex, Highs, MAiNGO from .build import AppsiBuilder @@ -30,3 +30,6 @@ def load(): SolverFactory.register(name='highs', doc='Automated persistent interface to Highs')( Highs ) + SolverFactory.register( + name='maingo', doc='Automated persistent interface to MAiNGO' + )(MAiNGO) From cf560a626c56aac304c269c02c06d783c42957c0 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Thu, 18 Apr 2024 09:31:50 +0200 Subject: [PATCH 27/39] Reformulate confusing comment --- pyomo/contrib/appsi/solvers/maingo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/appsi/solvers/maingo.py b/pyomo/contrib/appsi/solvers/maingo.py index d542542f543..f95a943bed3 100644 --- a/pyomo/contrib/appsi/solvers/maingo.py +++ b/pyomo/contrib/appsi/solvers/maingo.py @@ -369,8 +369,8 @@ def _remove_variables(self, variables: List[_GeneralVarData]): removed_maingo_vars += [self._pyomo_var_to_solver_var_id_map[id(var)]] del self._pyomo_var_to_solver_var_id_map[id(var)] + # Update _pyomo_var_to_solver_var_id_map to account for removed variables for pyomo_var, maingo_var_id in self._pyomo_var_to_solver_var_id_map.items(): - # How many variables before current var where removed? num_removed = 0 for removed_var in removed_maingo_vars: if removed_var <= maingo_var_id: From 3e477c929883d8a555fdde8afeb99b435f0be829 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Thu, 18 Apr 2024 09:33:45 +0200 Subject: [PATCH 28/39] Skip tests with problematic log and 1/x --- .../appsi/solvers/tests/test_persistent_solvers.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index f50461af373..b569b305a07 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -699,11 +699,11 @@ def test_fixed_vars_4( ): opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) opt.update_config.treat_fixed_vars_as_params = True - if not opt.available(): + if not opt.available() or opt_class in MAiNGO: raise unittest.SkipTest m = pe.ConcreteModel() m.x = pe.Var() - m.y = pe.Var(bounds=(1e-6, None)) + m.y = pe.Var() m.obj = pe.Objective(expr=m.x**2 + m.y**2) m.c1 = pe.Constraint(expr=m.x == 2 / m.y) m.y.fix(1) @@ -872,10 +872,10 @@ def test_exp(self, name: str, opt_class: Type[PersistentSolver], only_child_vars @parameterized.expand(input=_load_tests(nlp_solvers, only_child_vars_options)) def test_log(self, name: str, opt_class: Type[PersistentSolver], only_child_vars): opt = opt_class(only_child_vars=only_child_vars) - if not opt.available(): + if not opt.available() or opt_class in MAiNGO: raise unittest.SkipTest m = pe.ConcreteModel() - m.x = pe.Var(initialize=1, bounds=(1e-6, None)) + m.x = pe.Var(initialize=1) m.y = pe.Var() m.obj = pe.Objective(expr=m.x**2 + m.y**2) m.c1 = pe.Constraint(expr=m.y <= pe.log(m.x)) From d892473bca89ae62fd69b53eb0585b769cd91a60 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Thu, 18 Apr 2024 09:42:04 +0200 Subject: [PATCH 29/39] Add maingo to options --- pyomo/contrib/appsi/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/contrib/appsi/base.py b/pyomo/contrib/appsi/base.py index 201e5975ac9..13a841437ac 100644 --- a/pyomo/contrib/appsi/base.py +++ b/pyomo/contrib/appsi/base.py @@ -1665,7 +1665,7 @@ def license_is_valid(self) -> bool: @property def options(self): - for solver_name in ['gurobi', 'ipopt', 'cplex', 'cbc', 'highs']: + for solver_name in ['gurobi', 'ipopt', 'cplex', 'cbc', 'highs', 'maingo']: if hasattr(self, solver_name + '_options'): return getattr(self, solver_name + '_options') raise NotImplementedError('Could not find the correct options') @@ -1673,7 +1673,7 @@ def options(self): @options.setter def options(self, val): found = False - for solver_name in ['gurobi', 'ipopt', 'cplex', 'cbc', 'highs']: + for solver_name in ['gurobi', 'ipopt', 'cplex', 'cbc', 'highs', 'maingo']: if hasattr(self, solver_name + '_options'): setattr(self, solver_name + '_options', val) found = True From d026ee135f72788536b0586e6815fcc561a0ba86 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Thu, 18 Apr 2024 09:42:30 +0200 Subject: [PATCH 30/39] Rewrite check for MAiNGO --- .../solvers/tests/test_persistent_solvers.py | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index b569b305a07..2207ba70f4e 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -185,14 +185,14 @@ def test_range_constraint( res = opt.solve(m) self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, -1) - if not opt_class is MAiNGO: + if opt_class != MAiNGO: duals = opt.get_duals() self.assertAlmostEqual(duals[m.c], 1) m.obj.sense = pe.maximize res = opt.solve(m) self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, 1) - if not opt_class is MAiNGO: + if opt_class != MAiNGO: duals = opt.get_duals() self.assertAlmostEqual(duals[m.c], 1) @@ -211,7 +211,7 @@ def test_reduced_costs( self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, -1) self.assertAlmostEqual(m.y.value, -2) - if not opt_class is MAiNGO: + if opt_class != MAiNGO: rc = opt.get_reduced_costs() self.assertAlmostEqual(rc[m.x], 3) self.assertAlmostEqual(rc[m.y], 4) @@ -229,14 +229,14 @@ def test_reduced_costs2( res = opt.solve(m) self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, -1) - if not opt_class is MAiNGO: + if opt_class != MAiNGO: rc = opt.get_reduced_costs() self.assertAlmostEqual(rc[m.x], 1) m.obj.sense = pe.maximize res = opt.solve(m) self.assertEqual(res.termination_condition, TerminationCondition.optimal) self.assertAlmostEqual(m.x.value, 1) - if not opt_class is MAiNGO: + if opt_class != MAiNGO: rc = opt.get_reduced_costs() self.assertAlmostEqual(rc[m.x], 1) @@ -270,7 +270,7 @@ def test_param_changes( self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) self.assertTrue(res.best_objective_bound <= m.y.value) - if not opt_class is MAiNGO: + if opt_class != MAiNGO: duals = opt.get_duals() self.assertAlmostEqual(duals[m.c1], (1 + a1 / (a2 - a1))) self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) @@ -309,7 +309,7 @@ def test_immutable_param( self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) self.assertTrue(res.best_objective_bound <= m.y.value) - if not opt_class is MAiNGO: + if opt_class != MAiNGO: duals = opt.get_duals() self.assertAlmostEqual(duals[m.c1], (1 + a1 / (a2 - a1))) self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) @@ -344,7 +344,7 @@ def test_equality( self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) self.assertTrue(res.best_objective_bound <= m.y.value) - if not opt_class is MAiNGO: + if opt_class != MAiNGO: duals = opt.get_duals() self.assertAlmostEqual(duals[m.c1], (1 + a1 / (a2 - a1))) self.assertAlmostEqual(duals[m.c2], -a1 / (a2 - a1)) @@ -415,7 +415,7 @@ def test_no_objective( self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertEqual(res.best_feasible_objective, None) self.assertEqual(res.best_objective_bound, None) - if not opt_class is MAiNGO: + if opt_class != MAiNGO: duals = opt.get_duals() self.assertAlmostEqual(duals[m.c1], 0) self.assertAlmostEqual(duals[m.c2], 0) @@ -445,7 +445,7 @@ def test_add_remove_cons( self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) self.assertTrue(res.best_objective_bound <= m.y.value) - if not opt_class is MAiNGO: + if opt_class != MAiNGO: duals = opt.get_duals() self.assertAlmostEqual(duals[m.c1], -(1 + a1 / (a2 - a1))) self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) @@ -457,7 +457,7 @@ def test_add_remove_cons( self.assertAlmostEqual(m.y.value, a1 * (b3 - b1) / (a1 - a3) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) self.assertTrue(res.best_objective_bound <= m.y.value) - if not opt_class is MAiNGO: + if opt_class != MAiNGO: duals = opt.get_duals() self.assertAlmostEqual(duals[m.c1], -(1 + a1 / (a3 - a1))) self.assertAlmostEqual(duals[m.c2], 0) @@ -470,7 +470,7 @@ def test_add_remove_cons( self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) self.assertAlmostEqual(res.best_feasible_objective, m.y.value) self.assertTrue(res.best_objective_bound <= m.y.value) - if not opt_class is MAiNGO: + if opt_class != MAiNGO: duals = opt.get_duals() self.assertAlmostEqual(duals[m.c1], -(1 + a1 / (a2 - a1))) self.assertAlmostEqual(duals[m.c2], a1 / (a2 - a1)) @@ -512,7 +512,7 @@ def test_results_infeasible( RuntimeError, '.*does not currently have a valid solution.*' ): res.solution_loader.load_vars() - if not opt_class is MAiNGO: + if opt_class != MAiNGO: with self.assertRaisesRegex( RuntimeError, '.*does not currently have valid duals.*' ): @@ -537,7 +537,7 @@ def test_duals(self, name: str, opt_class: Type[PersistentSolver], only_child_va res = opt.solve(m) self.assertAlmostEqual(m.x.value, 1) self.assertAlmostEqual(m.y.value, 1) - if not opt_class is MAiNGO: + if opt_class != MAiNGO: duals = opt.get_duals() self.assertAlmostEqual(duals[m.c1], 0.5) self.assertAlmostEqual(duals[m.c2], 0.5) @@ -699,7 +699,7 @@ def test_fixed_vars_4( ): opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) opt.update_config.treat_fixed_vars_as_params = True - if not opt.available() or opt_class in MAiNGO: + if not opt.available() or opt_class == MAiNGO: raise unittest.SkipTest m = pe.ConcreteModel() m.x = pe.Var() @@ -792,7 +792,7 @@ def test_mutable_param_with_range( self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1, 6) self.assertAlmostEqual(res.best_feasible_objective, m.y.value, 6) self.assertTrue(res.best_objective_bound <= m.y.value + 1e-12) - if not opt_class is MAiNGO: + if opt_class != MAiNGO: duals = opt.get_duals() self.assertAlmostEqual(duals[m.con1], (1 + a1 / (a2 - a1)), 6) self.assertAlmostEqual(duals[m.con2], -a1 / (a2 - a1), 6) @@ -801,7 +801,7 @@ def test_mutable_param_with_range( self.assertAlmostEqual(m.y.value, a1 * (c2 - c1) / (a1 - a2) + c1, 6) self.assertAlmostEqual(res.best_feasible_objective, m.y.value, 6) self.assertTrue(res.best_objective_bound >= m.y.value - 1e-12) - if not opt_class is MAiNGO: + if opt_class != MAiNGO: duals = opt.get_duals() self.assertAlmostEqual(duals[m.con1], (1 + a1 / (a2 - a1)), 6) self.assertAlmostEqual(duals[m.con2], -a1 / (a2 - a1), 6) @@ -872,7 +872,7 @@ def test_exp(self, name: str, opt_class: Type[PersistentSolver], only_child_vars @parameterized.expand(input=_load_tests(nlp_solvers, only_child_vars_options)) def test_log(self, name: str, opt_class: Type[PersistentSolver], only_child_vars): opt = opt_class(only_child_vars=only_child_vars) - if not opt.available() or opt_class in MAiNGO: + if not opt.available() or opt_class == MAiNGO: raise unittest.SkipTest m = pe.ConcreteModel() m.x = pe.Var(initialize=1) @@ -1002,7 +1002,7 @@ def test_solution_loader( self.assertNotIn(m.x, primals) self.assertIn(m.y, primals) self.assertAlmostEqual(primals[m.y], 1) - if not opt_class is MAiNGO: + if opt_class != MAiNGO: reduced_costs = res.solution_loader.get_reduced_costs() self.assertIn(m.x, reduced_costs) self.assertIn(m.y, reduced_costs) @@ -1108,7 +1108,7 @@ def test_objective_changes( m.obj.sense = pe.maximize opt.config.load_solution = False res = opt.solve(m) - if not isinstance(opt, MAiNGO): + if opt_class != MAiNGO: self.assertIn( res.termination_condition, { @@ -1390,7 +1390,7 @@ def test_param_updates(self, name: str, opt_class: Type[PersistentSolver]): m.obj = pe.Objective(expr=m.y) m.c1 = pe.Constraint(expr=(0, m.y - m.a1 * m.x - m.b1, None)) m.c2 = pe.Constraint(expr=(None, -m.y + m.a2 * m.x + m.b2, 0)) - if not opt_class is MAiNGO: + if opt_class != MAiNGO: m.dual = pe.Suffix(direction=pe.Suffix.IMPORT) params_to_test = [(1, -1, 2, 1), (1, -2, 2, 1), (1, -1, 3, 1)] @@ -1403,7 +1403,7 @@ def test_param_updates(self, name: str, opt_class: Type[PersistentSolver]): pe.assert_optimal_termination(res) self.assertAlmostEqual(m.x.value, (b2 - b1) / (a1 - a2)) self.assertAlmostEqual(m.y.value, a1 * (b2 - b1) / (a1 - a2) + b1) - if not opt_class is MAiNGO: + if opt_class != MAiNGO: self.assertAlmostEqual(m.dual[m.c1], (1 + a1 / (a2 - a1))) self.assertAlmostEqual(m.dual[m.c2], a1 / (a2 - a1)) @@ -1416,14 +1416,14 @@ def test_load_solutions(self, name: str, opt_class: Type[PersistentSolver]): m.x = pe.Var() m.obj = pe.Objective(expr=m.x) m.c = pe.Constraint(expr=(-1, m.x, 1)) - if not opt_class is MAiNGO: + if opt_class != MAiNGO: m.dual = pe.Suffix(direction=pe.Suffix.IMPORT) res = opt.solve(m, load_solutions=False) pe.assert_optimal_termination(res) self.assertIsNone(m.x.value) - if not opt_class is MAiNGO: + if opt_class != MAiNGO: self.assertNotIn(m.c, m.dual) m.solutions.load_from(res) self.assertAlmostEqual(m.x.value, -1) - if not opt_class is MAiNGO: + if opt_class != MAiNGO: self.assertAlmostEqual(m.dual[m.c], 1) From cf3c9da2a17cff71953e2ced4f24f999835542ba Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Thu, 18 Apr 2024 12:26:32 +0200 Subject: [PATCH 31/39] Add ConfigDict for tolerances, change test tolerances --- pyomo/contrib/appsi/solvers/__init__.py | 2 +- pyomo/contrib/appsi/solvers/maingo.py | 60 ++++++++++++++----- .../solvers/tests/test_persistent_solvers.py | 27 ++++----- 3 files changed, 58 insertions(+), 31 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/__init__.py b/pyomo/contrib/appsi/solvers/__init__.py index c1ebdf28780..352571b98f8 100644 --- a/pyomo/contrib/appsi/solvers/__init__.py +++ b/pyomo/contrib/appsi/solvers/__init__.py @@ -15,4 +15,4 @@ from .cplex import Cplex from .highs import Highs from .wntr import Wntr, WntrResults -from .maingo import MAiNGO, MAiNGOTest +from .maingo import MAiNGO diff --git a/pyomo/contrib/appsi/solvers/maingo.py b/pyomo/contrib/appsi/solvers/maingo.py index f95a943bed3..944673be53d 100644 --- a/pyomo/contrib/appsi/solvers/maingo.py +++ b/pyomo/contrib/appsi/solvers/maingo.py @@ -25,7 +25,12 @@ ) from pyomo.contrib.appsi.cmodel import cmodel, cmodel_available from pyomo.common.collections import ComponentMap -from pyomo.common.config import ConfigValue, NonNegativeInt +from pyomo.common.config import ( + ConfigValue, + ConfigDict, + NonNegativeInt, + NonNegativeFloat, +) from pyomo.common.dependencies import attempt_import from pyomo.common.errors import PyomoException from pyomo.common.log import LogStream @@ -97,7 +102,41 @@ def __init__( implicit_domain=implicit_domain, visibility=visibility, ) + self.tolerances: ConfigDict = self.declare( + 'tolerances', ConfigDict(implicit=True) + ) + + self.tolerances.epsilonA: Optional[float] = self.tolerances.declare( + 'epsilonA', + ConfigValue( + domain=NonNegativeFloat, + default=1e-4, + description="Absolute optimality tolerance", + ), + ) + self.tolerances.epsilonR: Optional[float] = self.tolerances.declare( + 'epsilonR', + ConfigValue( + domain=NonNegativeFloat, + default=1e-4, + description="Relative optimality tolerance", + ), + ) + self.tolerances.deltaEq: Optional[float] = self.tolerances.declare( + 'deltaEq', + ConfigValue( + domain=NonNegativeFloat, default=1e-6, description="Equality tolerance" + ), + ) + self.tolerances.deltaIneq: Optional[float] = self.tolerances.declare( + 'deltaIneq', + ConfigValue( + domain=NonNegativeFloat, + default=1e-6, + description="Inequality tolerance", + ), + ) self.declare("logfile", ConfigValue(domain=str, default="")) self.declare("solver_output_logger", ConfigValue(default=logger)) self.declare( @@ -205,9 +244,10 @@ def _solve(self, timer: HierarchicalTimer): self._mymaingo.set_option("loggingDestination", 2) self._mymaingo.set_log_file_name(config.logfile) - self._mymaingo.set_option("epsilonA", 1e-4) - self._mymaingo.set_option("epsilonR", 1e-4) - self._set_maingo_options() + self._mymaingo.set_option("epsilonA", config.tolerances.epsilonA) + self._mymaingo.set_option("epsilonR", config.tolerances.epsilonR) + self._mymaingo.set_option("deltaEq", config.tolerances.deltaEq) + self._mymaingo.set_option("deltaIneq", config.tolerances.deltaIneq) if config.time_limit is not None: self._mymaingo.set_option("maxTime", config.time_limit) @@ -526,15 +566,3 @@ def update(self, timer: HierarchicalTimer = None): idmap=self._pyomo_var_to_solver_var_id_map, logger=logger, ) - - def _set_maingo_options(self): - pass - - -# Solver class with tighter tolerances for testing -class MAiNGOTest(MAiNGO): - def _set_maingo_options(self): - self._mymaingo.set_option("epsilonA", 1e-8) - self._mymaingo.set_option("epsilonR", 1e-8) - self._mymaingo.set_option("deltaIneq", 1e-9) - self._mymaingo.set_option("deltaEq", 1e-9) diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index 2207ba70f4e..d6df1710a03 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -17,8 +17,7 @@ parameterized = parameterized.parameterized from pyomo.contrib.appsi.base import TerminationCondition, Results, PersistentSolver from pyomo.contrib.appsi.cmodel import cmodel_available -from pyomo.contrib.appsi.solvers import Gurobi, Ipopt, Cplex, Cbc, Highs -from pyomo.contrib.appsi.solvers import MAiNGOTest as MAiNGO +from pyomo.contrib.appsi.solvers import Gurobi, Ipopt, Cplex, Cbc, Highs, MAiNGO from typing import Type from pyomo.core.expr.numeric_expr import LinearExpression import os @@ -866,8 +865,8 @@ def test_exp(self, name: str, opt_class: Type[PersistentSolver], only_child_vars m.obj = pe.Objective(expr=m.x**2 + m.y**2) m.c1 = pe.Constraint(expr=m.y >= pe.exp(m.x)) res = opt.solve(m) - self.assertAlmostEqual(m.x.value, -0.42630274815985264) - self.assertAlmostEqual(m.y.value, 0.6529186341994245) + self.assertAlmostEqual(m.x.value, -0.42630274815985264, 6) + self.assertAlmostEqual(m.y.value, 0.6529186341994245, 6) @parameterized.expand(input=_load_tests(nlp_solvers, only_child_vars_options)) def test_log(self, name: str, opt_class: Type[PersistentSolver], only_child_vars): @@ -1212,19 +1211,19 @@ def test_fixed_binaries( m.c = pe.Constraint(expr=m.y >= m.x) m.x.fix(0) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 0) + self.assertAlmostEqual(res.best_feasible_objective, 0, 6) m.x.fix(1) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 1) + self.assertAlmostEqual(res.best_feasible_objective, 1, 6) opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) opt.update_config.treat_fixed_vars_as_params = False m.x.fix(0) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 0) + self.assertAlmostEqual(res.best_feasible_objective, 0, 6) m.x.fix(1) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 1) + self.assertAlmostEqual(res.best_feasible_objective, 1, 6) @parameterized.expand(input=_load_tests(mip_solvers, only_child_vars_options)) def test_with_gdp( @@ -1248,16 +1247,16 @@ def test_with_gdp( pe.TransformationFactory("gdp.bigm").apply_to(m) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 1) - self.assertAlmostEqual(m.x.value, 0) - self.assertAlmostEqual(m.y.value, 1) + self.assertAlmostEqual(res.best_feasible_objective, 1, 6) + self.assertAlmostEqual(m.x.value, 0, 6) + self.assertAlmostEqual(m.y.value, 1, 6) opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) opt.use_extensions = True res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 1) - self.assertAlmostEqual(m.x.value, 0) - self.assertAlmostEqual(m.y.value, 1) + self.assertAlmostEqual(res.best_feasible_objective, 1, 6) + self.assertAlmostEqual(m.x.value, 0, 6) + self.assertAlmostEqual(m.y.value, 1, 6) @parameterized.expand(input=all_solvers) def test_variables_elsewhere(self, name: str, opt_class: Type[PersistentSolver]): From 7126fb7a0258524d4a1233fd16a5931ef064fe08 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Tue, 30 Apr 2024 10:57:09 +0200 Subject: [PATCH 32/39] Modified two tests --- .../appsi/solvers/tests/test_persistent_solvers.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index d6df1710a03..d38563844ff 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -1026,7 +1026,7 @@ def test_time_limit( self, name: str, opt_class: Type[PersistentSolver], only_child_vars ): opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) - if not opt.available(): + if not opt.available() or opt_class == MAiNGO: raise unittest.SkipTest from sys import platform @@ -1210,20 +1210,23 @@ def test_fixed_binaries( m.obj = pe.Objective(expr=m.y) m.c = pe.Constraint(expr=m.y >= m.x) m.x.fix(0) + + if type(opt) is MAiNGO: + opt.config.mip_gap = 1e-6 res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 0, 6) + self.assertAlmostEqual(res.best_feasible_objective, 0) m.x.fix(1) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 1, 6) + self.assertAlmostEqual(res.best_feasible_objective, 1) opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) opt.update_config.treat_fixed_vars_as_params = False m.x.fix(0) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 0, 6) + self.assertAlmostEqual(res.best_feasible_objective, 0) m.x.fix(1) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 1, 6) + self.assertAlmostEqual(res.best_feasible_objective, 1) @parameterized.expand(input=_load_tests(mip_solvers, only_child_vars_options)) def test_with_gdp( From 673fd372e55c54496cd01446e248ba9ab3d96024 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Tue, 30 Apr 2024 12:33:04 +0200 Subject: [PATCH 33/39] Modify test_persistent_solvers.py --- pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index d38563844ff..3db2ae3cba1 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -1211,7 +1211,7 @@ def test_fixed_binaries( m.c = pe.Constraint(expr=m.y >= m.x) m.x.fix(0) - if type(opt) is MAiNGO: + if opt_class == MAiNGO: opt.config.mip_gap = 1e-6 res = opt.solve(m) self.assertAlmostEqual(res.best_feasible_objective, 0) From 0127d29aeadf2034fde7b95b962d89c6c58488a6 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Tue, 30 Apr 2024 13:21:54 +0200 Subject: [PATCH 34/39] Set default mipgap higher --- pyomo/contrib/appsi/solvers/maingo.py | 4 ++-- pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/maingo.py b/pyomo/contrib/appsi/solvers/maingo.py index 944673be53d..e52130061f7 100644 --- a/pyomo/contrib/appsi/solvers/maingo.py +++ b/pyomo/contrib/appsi/solvers/maingo.py @@ -110,7 +110,7 @@ def __init__( 'epsilonA', ConfigValue( domain=NonNegativeFloat, - default=1e-4, + default=1e-5, description="Absolute optimality tolerance", ), ) @@ -118,7 +118,7 @@ def __init__( 'epsilonR', ConfigValue( domain=NonNegativeFloat, - default=1e-4, + default=1e-5, description="Relative optimality tolerance", ), ) diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index 3db2ae3cba1..6ab36ccc981 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -1211,8 +1211,6 @@ def test_fixed_binaries( m.c = pe.Constraint(expr=m.y >= m.x) m.x.fix(0) - if opt_class == MAiNGO: - opt.config.mip_gap = 1e-6 res = opt.solve(m) self.assertAlmostEqual(res.best_feasible_objective, 0) m.x.fix(1) From 389740c54cdae96ef27375fc2b03e59c9002d036 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Tue, 30 Apr 2024 13:42:54 +0200 Subject: [PATCH 35/39] Modify test_fixed_binaries --- .../appsi/solvers/tests/test_persistent_solvers.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index 6ab36ccc981..7ff193b38e4 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -1212,19 +1212,19 @@ def test_fixed_binaries( m.x.fix(0) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 0) + self.assertAlmostEqual(res.best_feasible_objective, 0, 6) m.x.fix(1) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 1) + self.assertAlmostEqual(res.best_feasible_objective, 1, 6) opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) opt.update_config.treat_fixed_vars_as_params = False m.x.fix(0) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 0) + self.assertAlmostEqual(res.best_feasible_objective, 0, 6) m.x.fix(1) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 1) + self.assertAlmostEqual(res.best_feasible_objective, 1, 6) @parameterized.expand(input=_load_tests(mip_solvers, only_child_vars_options)) def test_with_gdp( From 547b3015eed3157b3a4023a5e31ca53d1b598e57 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Tue, 30 Apr 2024 15:32:58 +0200 Subject: [PATCH 36/39] Set epsilonA for one test --- pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index 7ff193b38e4..7fa2a62a8be 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -1210,7 +1210,8 @@ def test_fixed_binaries( m.obj = pe.Objective(expr=m.y) m.c = pe.Constraint(expr=m.y >= m.x) m.x.fix(0) - + if type(opt) is MAiNGO: + opt.maingo_options["epsilonA"] = 1e-6 res = opt.solve(m) self.assertAlmostEqual(res.best_feasible_objective, 0, 6) m.x.fix(1) From 0c14380b675f55a49f238b675055a1557e191ce5 Mon Sep 17 00:00:00 2001 From: Clara Witte Date: Tue, 30 Apr 2024 16:02:17 +0200 Subject: [PATCH 37/39] Modify fixed_binary-test --- .../appsi/solvers/tests/test_persistent_solvers.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py index 7fa2a62a8be..67088297cf4 100644 --- a/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py +++ b/pyomo/contrib/appsi/solvers/tests/test_persistent_solvers.py @@ -1210,22 +1210,20 @@ def test_fixed_binaries( m.obj = pe.Objective(expr=m.y) m.c = pe.Constraint(expr=m.y >= m.x) m.x.fix(0) - if type(opt) is MAiNGO: - opt.maingo_options["epsilonA"] = 1e-6 res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 0, 6) + self.assertAlmostEqual(res.best_feasible_objective, 0, 5) m.x.fix(1) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 1, 6) + self.assertAlmostEqual(res.best_feasible_objective, 1, 5) opt: PersistentSolver = opt_class(only_child_vars=only_child_vars) opt.update_config.treat_fixed_vars_as_params = False m.x.fix(0) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 0, 6) + self.assertAlmostEqual(res.best_feasible_objective, 0, 5) m.x.fix(1) res = opt.solve(m) - self.assertAlmostEqual(res.best_feasible_objective, 1, 6) + self.assertAlmostEqual(res.best_feasible_objective, 1, 5) @parameterized.expand(input=_load_tests(mip_solvers, only_child_vars_options)) def test_with_gdp( From f76108584d5be2f12259055cfac0529a39c7e8c1 Mon Sep 17 00:00:00 2001 From: Miranda Mundt <55767766+mrmundt@users.noreply.github.com> Date: Wed, 8 May 2024 08:19:01 -0600 Subject: [PATCH 38/39] Create basic autodoc for MAiNGO --- .../appsi/appsi.solvers.maingo.rst | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 doc/OnlineDocs/library_reference/appsi/appsi.solvers.maingo.rst diff --git a/doc/OnlineDocs/library_reference/appsi/appsi.solvers.maingo.rst b/doc/OnlineDocs/library_reference/appsi/appsi.solvers.maingo.rst new file mode 100644 index 00000000000..21e61c38d51 --- /dev/null +++ b/doc/OnlineDocs/library_reference/appsi/appsi.solvers.maingo.rst @@ -0,0 +1,14 @@ +MAiNGO +====== + +.. autoclass:: pyomo.contrib.appsi.solvers.maingo.MAiNGOConfig + :members: + :inherited-members: + :undoc-members: + :show-inheritance: + +.. autoclass:: pyomo.contrib.appsi.solvers.maingo.MAiNGO + :members: + :inherited-members: + :undoc-members: + :show-inheritance: From fbd9a0ab2d8a73babc3d67acfb6cc71d4d92677e Mon Sep 17 00:00:00 2001 From: Miranda Mundt <55767766+mrmundt@users.noreply.github.com> Date: Wed, 8 May 2024 08:22:32 -0600 Subject: [PATCH 39/39] Update APPSI TOC --- doc/OnlineDocs/library_reference/appsi/appsi.solvers.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/OnlineDocs/library_reference/appsi/appsi.solvers.rst b/doc/OnlineDocs/library_reference/appsi/appsi.solvers.rst index 1c598d95628..f4dcb81b4be 100644 --- a/doc/OnlineDocs/library_reference/appsi/appsi.solvers.rst +++ b/doc/OnlineDocs/library_reference/appsi/appsi.solvers.rst @@ -13,3 +13,4 @@ Solvers appsi.solvers.cplex appsi.solvers.cbc appsi.solvers.highs + appsi.solvers.maingo