From e5524392a9c99208448fe863952586e545776927 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Mon, 18 Mar 2024 15:14:12 +0100 Subject: [PATCH 01/25] Add first draft of SCIP persistent solving --- pyomo/solvers/plugins/solvers/__init__.py | 2 + pyomo/solvers/plugins/solvers/scip_direct.py | 838 ++++++++++++++++++ .../plugins/solvers/scip_persistent.py | 185 ++++ pyomo/solvers/tests/checks/test_SCIPDirect.py | 335 +++++++ .../tests/checks/test_SCIPPersistent.py | 318 +++++++ pyomo/solvers/tests/solvers.py | 21 + 6 files changed, 1699 insertions(+) create mode 100644 pyomo/solvers/plugins/solvers/scip_direct.py create mode 100644 pyomo/solvers/plugins/solvers/scip_persistent.py create mode 100644 pyomo/solvers/tests/checks/test_SCIPDirect.py create mode 100644 pyomo/solvers/tests/checks/test_SCIPPersistent.py diff --git a/pyomo/solvers/plugins/solvers/__init__.py b/pyomo/solvers/plugins/solvers/__init__.py index 9b2507d876c..e8f4e00e31a 100644 --- a/pyomo/solvers/plugins/solvers/__init__.py +++ b/pyomo/solvers/plugins/solvers/__init__.py @@ -30,3 +30,5 @@ import pyomo.solvers.plugins.solvers.mosek_persistent import pyomo.solvers.plugins.solvers.xpress_direct import pyomo.solvers.plugins.solvers.xpress_persistent +import pyomo.solvers.plugins.solvers.scip_direct +import pyomo.solvers.plugins.solvers.scip_persistent diff --git a/pyomo/solvers/plugins/solvers/scip_direct.py b/pyomo/solvers/plugins/solvers/scip_direct.py new file mode 100644 index 00000000000..0aafb596007 --- /dev/null +++ b/pyomo/solvers/plugins/solvers/scip_direct.py @@ -0,0 +1,838 @@ +# ___________________________________________________________________________ +# +# 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 logging +import re +import sys + +from pyomo.common.collections import ComponentSet, ComponentMap, Bunch +from pyomo.common.tempfiles import TempfileManager +from pyomo.core import Var +from pyomo.core.expr.numeric_expr import ( + SumExpression, + ProductExpression, + UnaryFunctionExpression, + PowExpression, + DivisionExpression, +) +from pyomo.core.expr.numvalue import is_fixed +from pyomo.core.expr.numvalue import value +from pyomo.core.staleflag import StaleFlagManager +from pyomo.repn import generate_standard_repn +from pyomo.solvers.plugins.solvers.direct_solver import DirectSolver +from pyomo.solvers.plugins.solvers.direct_or_persistent_solver import ( + DirectOrPersistentSolver, +) +from pyomo.core.kernel.objective import minimize, maximize +from pyomo.opt.results.results_ import SolverResults +from pyomo.opt.results.solution import Solution, SolutionStatus +from pyomo.opt.results.solver import TerminationCondition, SolverStatus +from pyomo.opt.base import SolverFactory +from pyomo.core.base.suffix import Suffix + + +logger = logging.getLogger("pyomo.solvers") + + +class DegreeError(ValueError): + pass + + +def _is_numeric(x): + try: + float(x) + except ValueError: + return False + return True + + +@SolverFactory.register("scip_direct", doc="Direct python interface to SCIP") +class SCIPDirect(DirectSolver): + + def __init__(self, **kwds): + kwds["type"] = "scipdirect" + DirectSolver.__init__(self, **kwds) + self._init() + self._solver_model = None + + def _init(self): + try: + import pyscipopt + + self._scip = pyscipopt + self._python_api_exists = True + self._version = str(self._scip.Model().version()) + self._version_major = self._version.split(".")[0] + except ImportError: + self._python_api_exists = False + except Exception as e: + print("Import of pyscipopt failed - SCIP message=" + str(e) + "\n") + self._python_api_exists = False + + # Note: Undefined capabilities default to None + self._max_constraint_degree = None + self._max_obj_degree = 1 + self._capabilities.linear = True + self._capabilities.quadratic_objective = False + self._capabilities.quadratic_constraint = True + self._capabilities.integer = True + self._capabilities.sos1 = True + self._capabilities.sos2 = True + + # Dictionary used exclusively for SCIP, as we want the constraint expressions + self._pyomo_var_to_solver_var_expr_map = ComponentMap() + self._pyomo_con_to_solver_con_expr_map = dict() + + def _apply_solver(self): + StaleFlagManager.mark_all_as_stale() + + # Supress solver output if requested + if self._tee: + self._solver_model.hideOutput(quiet=False) + else: + self._solver_model.hideOutput(quiet=True) + + # Redirect solver output to a logfile if requested + if self._keepfiles: + # Only save log file when the user wants to keep it. + self._solver_model.setLogfile(self._log_file) + print("Solver log file: " + self._log_file) + + # Set user specified parameters + for key, option in self.options.items(): + try: + key_type = type(self._solver_model.getParam(key)) + except KeyError: + raise ValueError(f"Key {key} is an invalid parameter for SCIP") + + if key_type == str: + self._solver_model.setParam(key, option) + else: + if not _is_numeric(option): + raise ValueError( + f"Value {option} for parameter {key} is not a string and can't be converted to float" + ) + self._solver_model.setParam(key, float(option)) + + self._solver_model.optimize() + + # TODO: Check if this is even needed, or if it is sufficient to close the open file + # if self._keepfiles: + # self._solver_model.setLogfile(None) + + # FIXME: can we get a return code indicating if SCIP had a significant failure? + return Bunch(rc=None, log=None) + + def _get_expr_from_pyomo_repn(self, repn, max_degree=None): + referenced_vars = ComponentSet() + + new_expr = repn.constant + + if len(repn.linear_vars) > 0: + referenced_vars.update(repn.linear_vars) + new_expr += sum( + repn.linear_coefs[i] * self._pyomo_var_to_solver_var_expr_map[var] + for i, var in enumerate(repn.linear_vars) + ) + + for i, v in enumerate(repn.quadratic_vars): + x, y = v + new_expr += ( + repn.quadratic_coefs[i] + * self._pyomo_var_to_solver_var_expr_map[x] + * self._pyomo_var_to_solver_var_expr_map[y] + ) + referenced_vars.add(x) + referenced_vars.add(y) + + # TODO: Introduce handling on non-linear expressions + if repn.nonlinear_expr is not None: + + def get_nl_expr_recursively(pyomo_expr): + if not hasattr(pyomo_expr, "args"): + if not isinstance(pyomo_expr, Var): + return float(pyomo_expr) + else: + referenced_vars.add(pyomo_expr) + return self._pyomo_var_to_solver_var_expr_map[pyomo_expr] + scip_expr_list = [0 for i in range(pyomo_expr.nargs())] + for i in range(pyomo_expr.nargs()): + scip_expr_list[i] = get_nl_expr_recursively(pyomo_expr.args[i]) + if isinstance(pyomo_expr, PowExpression): + if len(scip_expr_list) != 2: + raise ValueError( + f"PowExpression has {len(scip_expr_list)} many terms instead of two!" + ) + return scip_expr_list[0] ** (scip_expr_list[1]) + elif isinstance(pyomo_expr, ProductExpression): + return self._scip.quickprod(scip_expr_list) + elif isinstance(pyomo_expr, SumExpression): + return self._scip.quicksum(scip_expr_list) + elif isinstance(pyomo_expr, DivisionExpression): + if len(scip_expr_list) != 2: + raise ValueError( + f"DivisonExpression has {len(scip_expr_list)} many terms instead of two!" + ) + return scip_expr_list[0] / scip_expr_list[1] + elif isinstance(pyomo_expr, UnaryFunctionExpression): + if len(scip_expr_list) != 1: + raise ValueError( + f"UnaryExpression has {len(scip_expr_list)} many terms instead of one!" + ) + if pyomo_expr.name == "sin": + return self._scip.sin(scip_expr_list[0]) + elif pyomo_expr.name == "cos": + return self._scip.cos(scip_expr_list[0]) + elif pyomo_expr.name == "exp": + return self._scip.exp(scip_expr_list[0]) + elif pyomo_expr.name == "log": + return self._scip.log(scip_expr_list[0]) + else: + raise NotImplementedError( + f"PySCIPOpt through Pyomo does not support the unary function {pyomo_expr.name}" + ) + else: + raise NotImplementedError( + f"PySCIPOpt through Pyomo does not yet support expression type {type(pyomo_expr)}" + ) + + new_expr += get_nl_expr_recursively(repn.nonlinear_expr) + + return new_expr, referenced_vars + + def _get_expr_from_pyomo_expr(self, expr, max_degree=None): + if max_degree is None or max_degree >= 2: + repn = generate_standard_repn(expr, quadratic=True) + else: + repn = generate_standard_repn(expr, quadratic=False) + + scip_expr, referenced_vars = self._get_expr_from_pyomo_repn(repn, max_degree) + + return scip_expr, referenced_vars + + def _scip_lb_ub_from_var(self, var): + if var.is_fixed(): + val = var.value + return val, val + if var.has_lb(): + lb = value(var.lb) + else: + lb = -self._solver_model.infinity() + if var.has_ub(): + ub = value(var.ub) + else: + ub = self._solver_model.infinity() + return lb, ub + + def _add_var(self, var): + varname = self._symbol_map.getSymbol(var, self._labeler) + vtype = self._scip_vtype_from_var(var) + lb, ub = self._scip_lb_ub_from_var(var) + + scip_var = self._solver_model.addVar(lb=lb, ub=ub, vtype=vtype, name=varname) + + self._pyomo_var_to_solver_var_expr_map[var] = scip_var + self._pyomo_var_to_solver_var_map[var] = scip_var.name + self._solver_var_to_pyomo_var_map[varname] = var + self._referenced_variables[var] = 0 + + def close(self): + """Frees SCIP resources used by this solver instance.""" + + if self._solver_model is not None: + self._solver_model.freeProb() + self._solver_model = None + + def __exit__(self, t, v, traceback): + super().__exit__(t, v, traceback) + self.close() + + def _set_instance(self, model, kwds={}): + DirectOrPersistentSolver._set_instance(self, model, kwds) + try: + self._solver_model = self._scip.Model() + except Exception: + e = sys.exc_info()[1] + msg = ( + "Unable to create SCIP model. " + "Have you installed PySCIPOpt correctly?\n\n\t" + + "Error message: {0}".format(e) + ) + raise Exception(msg) + + self._add_block(model) + + for var, n_ref in self._referenced_variables.items(): + if n_ref != 0: + if var.fixed: + if not self._output_fixed_variable_bounds: + raise ValueError( + "Encountered a fixed variable (%s) inside " + "an active objective or constraint " + "expression on model %s, which is usually " + "indicative of a preprocessing error. Use " + "the IO-option 'output_fixed_variable_bounds=True' " + "to suppress this error and fix the variable " + "by overwriting its bounds in the SCIP instance." + % (var.name, self._pyomo_model.name) + ) + + def _add_block(self, block): + DirectOrPersistentSolver._add_block(self, block) + + def _add_constraint(self, con): + if not con.active: + return None + + if is_fixed(con.body) and self._skip_trivial_constraints: + return None + + conname = self._symbol_map.getSymbol(con, self._labeler) + + if con._linear_canonical_form: + scip_expr, referenced_vars = self._get_expr_from_pyomo_repn( + con.canonical_form(), self._max_constraint_degree + ) + else: + scip_expr, referenced_vars = self._get_expr_from_pyomo_expr( + con.body, self._max_constraint_degree + ) + + if con.has_lb(): + if not is_fixed(con.lower): + raise ValueError( + "Lower bound of constraint {0} is not constant.".format(con) + ) + if con.has_ub(): + if not is_fixed(con.upper): + raise ValueError( + "Upper bound of constraint {0} is not constant.".format(con) + ) + + if con.equality: + scip_cons = self._solver_model.addCons( + scip_expr == value(con.lower), name=conname + ) + elif con.has_lb() and con.has_ub(): + scip_cons = self._solver_model.addCons( + value(con.lower) <= (scip_expr <= value(con.upper)), name=conname + ) + elif con.has_lb(): + scip_cons = self._solver_model.addCons( + value(con.lower) <= scip_expr, name=conname + ) + elif con.has_ub(): + scip_cons = self._solver_model.addCons( + scip_expr <= value(con.upper), name=conname + ) + else: + raise ValueError( + "Constraint does not have a lower " + "or an upper bound: {0} \n".format(con) + ) + + for var in referenced_vars: + self._referenced_variables[var] += 1 + self._vars_referenced_by_con[con] = referenced_vars + self._pyomo_con_to_solver_con_expr_map[con] = scip_cons + self._pyomo_con_to_solver_con_map[con] = scip_cons.name + self._solver_con_to_pyomo_con_map[conname] = con + + def _add_sos_constraint(self, con): + if not con.active: + return None + + conname = self._symbol_map.getSymbol(con, self._labeler) + level = con.level + if level not in [1, 2]: + raise ValueError(f"Solver does not support SOS level {level} constraints") + + scip_vars = [] + weights = [] + + self._vars_referenced_by_con[con] = ComponentSet() + + if hasattr(con, "get_items"): + # aml sos constraint + sos_items = list(con.get_items()) + else: + # kernel sos constraint + sos_items = list(con.items()) + + for v, w in sos_items: + self._vars_referenced_by_con[con].add(v) + scip_vars.append(self._pyomo_var_to_solver_var_expr_map[v]) + self._referenced_variables[v] += 1 + weights.append(w) + + if level == 1: + scip_cons = self._solver_model.addConsSOS1( + scip_vars, weights=weights, name=conname + ) + else: + scip_cons = self._solver_model.addConsSOS2( + scip_vars, weights=weights, name=conname + ) + self._pyomo_con_to_solver_con_expr_map[con] = scip_cons + self._pyomo_con_to_solver_con_map[con] = scip_cons.name + self._solver_con_to_pyomo_con_map[conname] = con + + def _scip_vtype_from_var(self, var): + """ + This function takes a pyomo variable and returns the appropriate SCIP variable type + :param var: pyomo.core.base.var.Var + :return: B, I, or C + """ + if var.is_binary(): + vtype = "B" + elif var.is_integer(): + vtype = "I" + elif var.is_continuous(): + vtype = "C" + else: + raise ValueError( + "Variable domain type is not recognized for {0}".format(var.domain) + ) + return vtype + + def _set_objective(self, obj): + if self._objective is not None: + for var in self._vars_referenced_by_obj: + self._referenced_variables[var] -= 1 + self._vars_referenced_by_obj = ComponentSet() + self._objective = None + + if obj.active is False: + raise ValueError("Cannot add inactive objective to solver.") + + if obj.sense == minimize: + sense = "minimize" + elif obj.sense == maximize: + sense = "maximize" + else: + raise ValueError("Objective sense is not recognized: {0}".format(obj.sense)) + + scip_expr, referenced_vars = self._get_expr_from_pyomo_expr( + obj.expr, self._max_obj_degree + ) + + for var in referenced_vars: + self._referenced_variables[var] += 1 + + self._solver_model.setObjective(scip_expr, sense=sense) + self._objective = obj + self._vars_referenced_by_obj = referenced_vars + + self._needs_updated = True + + def _postsolve(self): + # the only suffixes that we extract from SCIP are + # constraint duals, constraint slacks, and variable + # reduced-costs. scan through the solver suffix list + # and throw an exception if the user has specified + # any others. + extract_duals = False + extract_slacks = False + extract_reduced_costs = False + for suffix in self._suffixes: + flag = False + if re.match(suffix, "dual"): + extract_duals = True + flag = True + if re.match(suffix, "slack"): + extract_slacks = True + flag = True + if re.match(suffix, "rc"): + extract_reduced_costs = True + flag = True + if not flag: + raise RuntimeError( + "***The scip_direct solver plugin cannot extract solution suffix=" + + suffix + ) + + scip = self._solver_model + status = scip.getStatus() + scip_vars = scip.getVars() + n_bin_vars = sum([scip_var.vtype() == "BINARY" for scip_var in scip_vars]) + n_int_vars = sum([scip_var.vtype() == "INTEGER" for scip_var in scip_vars]) + n_con_vars = sum([scip_var.vtype() == "CONTINUOUS" for scip_var in scip_vars]) + + if n_bin_vars + n_int_vars > 0: + if extract_reduced_costs: + logger.warning("Cannot get reduced costs for MIP.") + if extract_duals: + logger.warning("Cannot get duals for MIP.") + extract_reduced_costs = False + extract_duals = False + + self.results = SolverResults() + soln = Solution() + + self.results.solver.name = f"SCIP{self._version}" + self.results.solver.wallclock_time = scip.getSolvingTime() + + if scip.getStage() == 1: # SCIP Model is created but not yet optimized + self.results.solver.status = SolverStatus.aborted + self.results.solver.termination_message = ( + "Model is loaded, but no solution information is available." + ) + self.results.solver.termination_condition = TerminationCondition.error + soln.status = SolutionStatus.unknown + elif status == "optimal": # optimal + self.results.solver.status = SolverStatus.ok + self.results.solver.termination_message = ( + "Model was solved to optimality (subject to tolerances), " + "and an optimal solution is available." + ) + self.results.solver.termination_condition = TerminationCondition.optimal + soln.status = SolutionStatus.optimal + elif status == "infeasible": + self.results.solver.status = SolverStatus.warning + self.results.solver.termination_message = ( + "Model was proven to be infeasible" + ) + self.results.solver.termination_condition = TerminationCondition.infeasible + soln.status = SolutionStatus.infeasible + elif status == "inforunbd": + self.results.solver.status = SolverStatus.warning + self.results.solver.termination_message = ( + "Problem proven to be infeasible or unbounded." + ) + self.results.solver.termination_condition = ( + TerminationCondition.infeasibleOrUnbounded + ) + soln.status = SolutionStatus.unsure + elif status == "unbounded": + self.results.solver.status = SolverStatus.warning + self.results.solver.termination_message = ( + "Model was proven to be unbounded." + ) + self.results.solver.termination_condition = TerminationCondition.unbounded + soln.status = SolutionStatus.unbounded + elif status == "gaplimit": + self.results.solver.status = SolverStatus.aborted + self.results.solver.termination_message = ( + "Optimization terminated because the gap dropped below " + "the value specified in the " + "limits/gap parameter." + ) + self.results.solver.termination_condition = TerminationCondition.unknown + soln.status = SolutionStatus.stoppedByLimit + elif status == "stallnodelimit": + self.results.solver.status = SolverStatus.aborted + self.results.solver.termination_message = ( + "Optimization terminated because the stalling node limit " + "exceeded the value specified in the " + "limits/stallnodes parameter." + ) + self.results.solver.termination_condition = TerminationCondition.unknown + soln.status = SolutionStatus.stoppedByLimit + elif status == "restartlimit": + self.results.solver.status = SolverStatus.aborted + self.results.solver.termination_message = ( + "Optimization terminated because the total number of restarts " + "exceeded the value specified in the " + "limits/restarts parameter." + ) + self.results.solver.termination_condition = TerminationCondition.unknown + soln.status = SolutionStatus.stoppedByLimit + elif status == "nodelimit" or status == "totalnodelimit": + self.results.solver.status = SolverStatus.aborted + self.results.solver.termination_message = ( + "Optimization terminated because the number of " + "branch-and-cut nodes explored exceeded the limits specified " + "in the limits/nodes or limits/totalnodes parameter" + ) + self.results.solver.termination_condition = ( + TerminationCondition.maxEvaluations + ) + soln.status = SolutionStatus.stoppedByLimit + elif status == "timelimit": + self.results.solver.status = SolverStatus.aborted + self.results.solver.termination_message = ( + "Optimization terminated because the time expended exceeded " + "the value specified in the limits/time parameter." + ) + self.results.solver.termination_condition = ( + TerminationCondition.maxTimeLimit + ) + soln.status = SolutionStatus.stoppedByLimit + elif status == "sollimit" or status == "bestsollimit": + self.results.solver.status = SolverStatus.aborted + self.results.solver.termination_message = ( + "Optimization terminated because the number of solutions found " + "reached the value specified in the limits/solutions or" + "limits/bestsol parameter." + ) + self.results.solver.termination_condition = TerminationCondition.unknown + soln.status = SolutionStatus.stoppedByLimit + elif status == "memlimit": + self.results.solver.status = SolverStatus.aborted + self.results.solver.termination_message = ( + "Optimization terminated because the memory used exceeded " + "the value specified in the limits/memory parameter." + ) + self.results.solver.termination_condition = TerminationCondition.unknown + soln.status = SolutionStatus.stoppedByLimit + elif status == "userinterrupt": + self.results.solver.status = SolverStatus.aborted + self.results.solver.termination_message = ( + "Optimization was terminated by the user." + ) + self.results.solver.termination_condition = TerminationCondition.error + soln.status = SolutionStatus.error + else: + self.results.solver.status = SolverStatus.error + self.results.solver.termination_message = ( + "Unhandled SCIP status (" + str(status) + ")" + ) + self.results.solver.termination_condition = TerminationCondition.error + soln.status = SolutionStatus.error + + self.results.problem.name = scip.getProbName() + + if scip.getObjectiveSense() == "minimize": + self.results.problem.sense = minimize + elif scip.getObjectiveSense() == "maximize": + self.results.problem.sense = maximize + else: + raise RuntimeError( + f"Unrecognized SCIP objective sense: {scip.getObjectiveSense()}" + ) + + self.results.problem.upper_bound = None + self.results.problem.lower_bound = None + if scip.getNSols() > 0: + scip_has_sol = True + else: + scip_has_sol = False + if not scip_has_sol and (status == "inforunbd" or status == "infeasible"): + pass + else: + if n_bin_vars + n_int_vars == 0: + self.results.problem.upper_bound = scip.getObjVal() + self.results.problem.lower_bound = scip.getObjVal() + elif scip.getObjectiveSense() == "minimize": # minimizing + if scip_has_sol: + self.results.problem.upper_bound = scip.getObjVal() + else: + self.results.problem.upper_bound = scip.infinity() + self.results.problem.lower_bound = scip.getDualbound() + else: # maximizing + self.results.problem.upper_bound = scip.getDualbound() + if scip_has_sol: + self.results.problem.lower_bound = scip.getObjVal() + else: + self.results.problem.lower_bound = -scip.infinity() + + try: + soln.gap = ( + self.results.problem.upper_bound - self.results.problem.lower_bound + ) + except TypeError: + soln.gap = None + + # TODO: Should these values be of the transformed or the original problem? + self.results.problem.number_of_constraints = scip.getNConss() + # self.results.problem.number_of_nonzeros = None + self.results.problem.number_of_variables = scip.getNVars() + self.results.problem.number_of_binary_variables = n_bin_vars + self.results.problem.number_of_integer_variables = n_int_vars + self.results.problem.number_of_continuous_variables = n_con_vars + self.results.problem.number_of_objectives = 1 + self.results.problem.number_of_solutions = scip.getNSols() + + # if a solve was stopped by a limit, we still need to check to + # see if there is a solution available - this may not always + # be the case, both in LP and MIP contexts. + if self._save_results: + """ + This code in this if statement is only needed for backwards compatibility. It is more efficient to set + _save_results to False and use load_vars, load_duals, etc. + """ + if scip.getNSols() > 0: + soln_variables = soln.variable + soln_constraints = soln.constraint + scip_sol = scip.getBestSol() + + scip_vars = scip.getVars() + scip_var_names = [scip_var.name for scip_var in scip_vars] + var_names = set(self._solver_var_to_pyomo_var_map.keys()) + assert set(scip_var_names) == var_names + var_vals = [scip.getVal(scip_var) for scip_var in scip_vars] + + for scip_var, val, name in zip(scip_vars, var_vals, scip_var_names): + pyomo_var = self._solver_var_to_pyomo_var_map[name] + if self._referenced_variables[pyomo_var] > 0: + soln_variables[name] = {"Value": val} + + if extract_reduced_costs: + vals = [scip.getVarRedcost(scip_var) for scip_var in scip_vars] + for scip_var, val, name in zip(scip_vars, vals, scip_var_names): + pyomo_var = self._solver_var_to_pyomo_var_map[name] + if self._referenced_variables[pyomo_var] > 0: + soln_variables[name]["Rc"] = val + + if extract_duals or extract_slacks: + scip_cons = scip.getConss() + con_names = [cons.name for cons in scip_cons] + assert set(self._solver_con_to_pyomo_con_map.keys()) == set( + con_names + ) + for name in con_names: + soln_constraints[name] = {} + + if extract_duals: + vals = [scip.getDualSolVal(con) for con in scip_cons] + for val, name in zip(vals, con_names): + soln_constraints[name]["Dual"] = val + + if extract_slacks: + vals = [scip.getSlack(con, scip_sol) for con in scip_cons] + for val, name in zip(vals, con_names): + soln_constraints[name]["Slack"] = val + + elif self._load_solutions: + if scip.getNSols() > 0: + self.load_vars() + + if extract_reduced_costs: + self._load_rc() + + if extract_duals: + self._load_duals() + + if extract_slacks: + self._load_slacks() + + self.results.solution.insert(soln) + + # finally, clean any temporary files registered with the temp file + # manager, created populated *directly* by this plugin. + TempfileManager.pop(remove=not self._keepfiles) + + return DirectOrPersistentSolver._postsolve(self) + + def warm_start_capable(self): + return True + + def _warm_start(self): + scip_sol = self._solver_model.createSol() + for pyomo_var, scip_var in self._pyomo_var_to_solver_var_expr_map.items(): + if pyomo_var.value is not None: + scip_sol[scip_var] = value(pyomo_var) + self._solver_model.trySol(scip_sol, free=True) + + def _load_vars(self, vars_to_load=None): + var_map = self._pyomo_var_to_solver_var_expr_map + ref_vars = self._referenced_variables + if vars_to_load is None: + vars_to_load = var_map.keys() + + scip_vars_to_load = [var_map[pyomo_var] for pyomo_var in vars_to_load] + vals = [self._solver_model.getVal(scip_var) for scip_var in scip_vars_to_load] + + for var, val in zip(vars_to_load, vals): + if ref_vars[var] > 0: + var.set_value(val, skip_validation=True) + + def _load_rc(self, vars_to_load=None): + if not hasattr(self._pyomo_model, "rc"): + self._pyomo_model.rc = Suffix(direction=Suffix.IMPORT) + var_map = self._pyomo_var_to_solver_var_expr_map + ref_vars = self._referenced_variables + rc = self._pyomo_model.rc + if vars_to_load is None: + vars_to_load = var_map.keys() + + scip_vars_to_load = [var_map[pyomo_var] for pyomo_var in vars_to_load] + vals = [ + self._solver_model.getVarRedcost(scip_var) for scip_var in scip_vars_to_load + ] + + for var, val in zip(vars_to_load, vals): + if ref_vars[var] > 0: + rc[var] = val + + def _load_duals(self, cons_to_load=None): + if not hasattr(self._pyomo_model, "dual"): + self._pyomo_model.dual = Suffix(direction=Suffix.IMPORT) + con_map = self._pyomo_con_to_solver_con_map + reverse_con_map = self._solver_con_to_pyomo_con_map + dual = self._pyomo_model.dual + scip_cons = self._solver_model.getConss() + + if cons_to_load is None: + con_names = [con.name for con in scip_cons] + vals = [self._solver_model.getDualSolVal(con) for con in scip_cons] + else: + con_names = set([con_map[pyomo_con] for pyomo_con in cons_to_load]) + scip_cons_to_load = [con for con in scip_cons if con.name in con_names] + vals = [self._solver_model.getDualSolVal(con) for con in scip_cons_to_load] + + for i, con_name in enumerate(con_names): + pyomo_con = reverse_con_map[con_name] + dual[pyomo_con] = vals[i] + + def _load_slacks(self, cons_to_load=None): + if not hasattr(self._pyomo_model, "slack"): + self._pyomo_model.slack = Suffix(direction=Suffix.IMPORT) + con_map = self._pyomo_con_to_solver_con_map + reverse_con_map = self._solver_con_to_pyomo_con_map + slack = self._pyomo_model.slack + scip_cons = self._solver_model.getConss() + scip_sol = self._solver_model.getBestSol() + + if cons_to_load is None: + con_names = [con.name for con in scip_cons] + vals = [self._solver_model.getSlack(con, scip_sol) for con in scip_cons] + else: + con_names = set([con_map[pyomo_con] for pyomo_con in cons_to_load]) + scip_cons_to_load = [con for con in scip_cons if con.name in con_names] + vals = [ + self._solver_model.getSlack(con, scip_sol) for con in scip_cons_to_load + ] + + for i, con_name in enumerate(con_names): + pyomo_con = reverse_con_map[con_name] + slack[pyomo_con] = vals[i] + + def load_duals(self, cons_to_load=None): + """ + Load the duals into the 'dual' suffix. The 'dual' suffix must live on the parent model. + + Parameters + ---------- + cons_to_load: list of Constraint + """ + self._load_duals(cons_to_load) + + def load_rc(self, vars_to_load): + """ + Load the reduced costs into the 'rc' suffix. The 'rc' suffix must live on the parent model. + + Parameters + ---------- + vars_to_load: list of Var + """ + self._load_rc(vars_to_load) + + def load_slacks(self, cons_to_load=None): + """ + Load the values of the slack variables into the 'slack' suffix. The 'slack' suffix must live on the parent + model. + + Parameters + ---------- + cons_to_load: list of Constraint + """ + self._load_slacks(cons_to_load) diff --git a/pyomo/solvers/plugins/solvers/scip_persistent.py b/pyomo/solvers/plugins/solvers/scip_persistent.py new file mode 100644 index 00000000000..408aa84633f --- /dev/null +++ b/pyomo/solvers/plugins/solvers/scip_persistent.py @@ -0,0 +1,185 @@ +# ___________________________________________________________________________ +# +# 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 pyomo.solvers.plugins.solvers.scip_direct import SCIPDirect +from pyomo.solvers.plugins.solvers.persistent_solver import PersistentSolver +from pyomo.opt.base import SolverFactory + + +@SolverFactory.register("scip_persistent", doc="Persistent python interface to SCIP") +class SCIPPersistent(PersistentSolver, SCIPDirect): + """ + A class that provides a persistent interface to SCIP. Direct solver interfaces do not use any file io. + Rather, they interface directly with the python bindings for the specific solver. Persistent solver interfaces + are similar except that they "remember" their model. Thus, persistent solver interfaces allow incremental changes + to the solver model (e.g., the gurobi python model or the cplex python model). Note that users are responsible + for notifying the persistent solver interfaces when changes are made to the corresponding pyomo model. + + Keyword Arguments + ----------------- + model: ConcreteModel + Passing a model to the constructor is equivalent to calling the set_instance method. + type: str + String indicating the class type of the solver instance. + name: str + String representing either the class type of the solver instance or an assigned name. + doc: str + Documentation for the solver + options: dict + Dictionary of solver options + """ + + def __init__(self, **kwds): + kwds["type"] = "scip_persistent" + PersistentSolver.__init__(self, **kwds) + SCIPDirect._init(self) + + self._pyomo_model = kwds.pop("model", None) + if self._pyomo_model is not None: + self.set_instance(self._pyomo_model, **kwds) + + def _remove_constraint(self, solver_conname): + con = self._solver_con_to_pyomo_con_map[solver_conname] + scip_con = self._pyomo_con_to_solver_con_expr_map[con] + self._solver_model.delCons(scip_con) + + def _remove_sos_constraint(self, solver_sos_conname): + con = self._solver_con_to_pyomo_con_map[solver_sos_conname] + scip_con = self._pyomo_con_to_solver_con_expr_map[con] + self._solver_model.delCons(scip_con) + + def _remove_var(self, solver_varname): + var = self._solver_var_to_pyomo_var_map[solver_varname] + scip_var = self._pyomo_var_to_solver_var_expr_map[var] + self._solver_model.delVar(scip_var) + + def _warm_start(self): + SCIPDirect._warm_start(self) + + def update_var(self, var): + """Update a single variable in the solver's model. + + This will update bounds, fix/unfix the variable as needed, and + update the variable type. + + Parameters + ---------- + var: Var (scalar Var or single _VarData) + + """ + # see PR #366 for discussion about handling indexed + # objects and keeping compatibility with the + # pyomo.kernel objects + # if var.is_indexed(): + # for child_var in var.values(): + # self.compile_var(child_var) + # return + if var not in self._pyomo_var_to_solver_var_map: + raise ValueError( + "The Var provided to compile_var needs to be added first: {0}".format( + var + ) + ) + scip_var = self._pyomo_var_to_solver_var_map[var] + vtype = self._scip_vtype_from_var(var) + lb, ub = self._scip_lb_ub_from_var(var) + + self._solver_model.chgVarLb(scip_var, lb) + self._solver_model.chgVarUb(scip_var, ub) + self._solver_model.chgVarType(scip_var, vtype) + + def write(self, filename, filetype=""): + """ + Write the model to a file (e.g., and lp file). + + Parameters + ---------- + filename: str + Name of the file to which the model should be written. + filetype: str + The file type (e.g., lp). + """ + self._solver_model.writeProblem(filename + filetype) + + def set_scip_param(self, param, val): + """ + Set a SCIP parameter. + + Parameters + ---------- + param: str + The SCIP parameter to set. Options include any SCIP parameter. + Please see the SCIP documentation for options. + val: any + The value to set the parameter to. See SCIP documentation for possible values. + """ + self._solver_model.setParam(param, val) + + def get_scip_param(self, param): + """ + Get the value of the SCIP parameter. + + Parameters + ---------- + param: str or int or float + The SCIP parameter to get the value of. See SCIP documentation for possible options. + """ + return self._solver_model.getParam(param) + + def _add_column(self, var, obj_coef, constraints, coefficients): + """Add a column to the solver's model + + This will add the Pyomo variable var to the solver's + model, and put the coefficients on the associated + constraints in the solver model. If the obj_coef is + not zero, it will add obj_coef*var to the objective + of the solver's model. + + Parameters + ---------- + var: Var (scalar Var or single _VarData) + obj_coef: float + constraints: list of solver constraints + coefficients: list of coefficients to put on var in the associated constraint + """ + + # Set-up add var + varname = self._symbol_map.getSymbol(var, self._labeler) + vtype = self._scip_vtype_from_var(var) + lb, ub = self._scip_lb_ub_from_var(var) + + # Add the variable to the model and then to all the constraints + scip_var = self._solver_model.addVar(lb=lb, ub=ub, vtype=vtype, name=varname) + self._pyomo_var_to_solver_var_expr_map[var] = scip_var + self._solver_var_to_pyomo_var_map[varname] = var + self._referenced_variables[var] = len(coefficients) + + # Get the SCIP cons by passing through two dictionaries + pyomo_cons = [self._solver_con_to_pyomo_con_map[con] for con in constraints] + scip_cons = [ + self._pyomo_con_to_solver_con_expr_map[pyomo_con] + for pyomo_con in pyomo_cons + ] + + for i, scip_con in enumerate(scip_cons): + if not scip_con.isLinear(): + raise ValueError( + "_add_column functionality not supported for non-linear constraints" + ) + self._solver_model.addConsCoeff(scip_con, scip_var, coefficients[i]) + con = self._solver_con_to_pyomo_con_map[scip_con.name] + self._vars_referenced_by_con[con].add(var) + + sense = self._solver_model.getObjectiveSense() + self._solver_model.setObjective(obj_coef * scip_var, sense=sense, clear=False) + + def reset(self): + self._solver_model.freeTransform() diff --git a/pyomo/solvers/tests/checks/test_SCIPDirect.py b/pyomo/solvers/tests/checks/test_SCIPDirect.py new file mode 100644 index 00000000000..ee37f5ddcc8 --- /dev/null +++ b/pyomo/solvers/tests/checks/test_SCIPDirect.py @@ -0,0 +1,335 @@ +# ___________________________________________________________________________ +# +# 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 sys + +import pyomo.common.unittest as unittest + +from pyomo.environ import ( + ConcreteModel, + AbstractModel, + Var, + Objective, + Block, + Constraint, + Suffix, + NonNegativeIntegers, + NonNegativeReals, + Integers, + Binary, + value, +) +from pyomo.opt import SolverFactory, TerminationCondition, SolutionStatus + +try: + import pyscipopt + + scip_available = True +except ImportError: + scip_available = False + + +class SCIPDirectTests(unittest.TestCase): + def setUp(self): + self.stderr = sys.stderr + sys.stderr = None + + def tearDown(self): + sys.stderr = self.stderr + + @unittest.skipIf(not scip_available, "The SCIP python bindings are not available") + def test_infeasible_lp(self): + with SolverFactory("scip_direct", solver_io="python") as opt: + model = ConcreteModel() + model.X = Var(within=NonNegativeReals) + model.C1 = Constraint(expr=model.X == 1) + model.C2 = Constraint(expr=model.X == 2) + model.O = Objective(expr=model.X) + + results = opt.solve(model) + + self.assertEqual( + results.solver.termination_condition, TerminationCondition.infeasible + ) + + @unittest.skipIf(not scip_available, "The SCIP python bindings are not available") + def test_unbounded_lp(self): + with SolverFactory("scip_direct", solver_io="python") as opt: + model = ConcreteModel() + model.X = Var() + model.O = Objective(expr=model.X) + + results = opt.solve(model) + + self.assertIn( + results.solver.termination_condition, + ( + TerminationCondition.unbounded, + TerminationCondition.infeasibleOrUnbounded, + ), + ) + + @unittest.skipIf(not scip_available, "The SCIP python bindings are not available") + def test_optimal_lp(self): + with SolverFactory("scip_direct", solver_io="python") as opt: + model = ConcreteModel() + model.X = Var(within=NonNegativeReals) + model.O = Objective(expr=model.X) + + results = opt.solve(model, load_solutions=False) + + self.assertEqual(results.solution.status, SolutionStatus.optimal) + + @unittest.skipIf(not scip_available, "The SCIP python bindings are not available") + def test_get_duals_lp(self): + with SolverFactory("scip_direct", solver_io="python") as opt: + model = ConcreteModel() + model.X = Var(within=NonNegativeReals) + model.Y = Var(within=NonNegativeReals) + + model.C1 = Constraint(expr=2 * model.X + model.Y >= 8) + model.C2 = Constraint(expr=model.X + 3 * model.Y >= 6) + + model.O = Objective(expr=model.X + model.Y) + + results = opt.solve(model, suffixes=["dual"], load_solutions=False) + + model.dual = Suffix(direction=Suffix.IMPORT) + model.solutions.load_from(results) + + self.assertAlmostEqual(model.dual[model.C1], 0.4) + self.assertAlmostEqual(model.dual[model.C2], 0.2) + + @unittest.skipIf(not scip_available, "The SCIP python bindings are not available") + def test_infeasible_mip(self): + with SolverFactory("scip_direct", solver_io="python") as opt: + model = ConcreteModel() + model.X = Var(within=NonNegativeIntegers) + model.C1 = Constraint(expr=model.X == 1) + model.C2 = Constraint(expr=model.X == 2) + model.O = Objective(expr=model.X) + + results = opt.solve(model) + + self.assertEqual( + results.solver.termination_condition, TerminationCondition.infeasible + ) + + @unittest.skipIf(not scip_available, "The SCIP python bindings are not available") + def test_unbounded_mip(self): + with SolverFactory("scip_direct", solver_io="python") as opt: + model = AbstractModel() + model.X = Var(within=Integers) + model.O = Objective(expr=model.X) + + instance = model.create_instance() + results = opt.solve(instance) + + self.assertIn( + results.solver.termination_condition, + ( + TerminationCondition.unbounded, + TerminationCondition.infeasibleOrUnbounded, + ), + ) + + @unittest.skipIf(not scip_available, "The SCIP python bindings are not available") + def test_optimal_mip(self): + with SolverFactory("scip_direct", solver_io="python") as opt: + model = ConcreteModel() + model.X = Var(within=NonNegativeIntegers) + model.O = Objective(expr=model.X) + + results = opt.solve(model, load_solutions=False) + + self.assertEqual(results.solution.status, SolutionStatus.optimal) + + +@unittest.skipIf(not scip_available, "The SCIP python bindings are not available") +class TestAddVar(unittest.TestCase): + def test_add_single_variable(self): + """Test that the variable is added correctly to `solver_model`.""" + model = ConcreteModel() + + opt = SolverFactory("scip_direct", solver_io="python") + opt._set_instance(model) + + self.assertEqual(opt._solver_model.getNVars(), 0) + + model.X = Var(within=Binary) + + opt._add_var(model.X) + + self.assertEqual(opt._solver_model.getNVars(), 1) + self.assertEqual(opt._solver_model.getVars()[0].vtype(), "BINARY") + + def test_add_block_containing_single_variable(self): + """Test that the variable is added correctly to `solver_model`.""" + model = ConcreteModel() + + opt = SolverFactory("scip_direct", solver_io="python") + opt._set_instance(model) + + self.assertEqual(opt._solver_model.getNVars(), 0) + + model.X = Var(within=Binary) + + opt._add_block(model) + + self.assertEqual(opt._solver_model.getNVars(), 1) + self.assertEqual(opt._solver_model.getVars()[0].vtype(), "BINARY") + + def test_add_block_containing_multiple_variables(self): + """Test that: + - The variable is added correctly to `solver_model` + - Fixed variable bounds are set correctly + """ + model = ConcreteModel() + + opt = SolverFactory("scip_direct", solver_io="python") + opt._set_instance(model) + + self.assertEqual(opt._solver_model.getNVars(), 0) + + model.X1 = Var(within=Binary) + model.X2 = Var(within=NonNegativeReals) + model.X3 = Var(within=NonNegativeIntegers) + + model.X3.fix(5) + + opt._add_block(model) + + self.assertEqual(opt._solver_model.getNVars(), 3) + scip_vars = opt._solver_model.getVars() + vtypes = [scip_var.vtype() for scip_var in scip_vars] + assert "BINARY" in vtypes and "CONTINUOUS" in vtypes and "INTEGER" in vtypes + lbs = [scip_var.getLbGlobal() for scip_var in scip_vars] + ubs = [scip_var.getUbGlobal() for scip_var in scip_vars] + assert 0 in lbs and 5 in lbs + assert ( + 1 in ubs + and 5 in ubs + and any([opt._solver_model.isInfinity(ub) for ub in ubs]) + ) + + +@unittest.skipIf(not scip_available, "The SCIP python bindings are not available") +class TestAddCon(unittest.TestCase): + def test_add_single_constraint(self): + model = ConcreteModel() + model.X = Var(within=Binary) + + opt = SolverFactory("scip_direct", solver_io="python") + opt._set_instance(model) + + self.assertEqual(opt._solver_model.getNConss(), 0) + + model.C = Constraint(expr=model.X == 1) + + opt._add_constraint(model.C) + + self.assertEqual(opt._solver_model.getNConss(), 1) + con = opt._solver_model.getConss()[0] + self.assertEqual(con.isLinear(), 1) + self.assertEqual(opt._solver_model.getRhs(con), 1) + + def test_add_block_containing_single_constraint(self): + model = ConcreteModel() + model.X = Var(within=Binary) + + opt = SolverFactory("scip_direct", solver_io="python") + opt._set_instance(model) + + self.assertEqual(opt._solver_model.getNConss(), 0) + + model.B = Block() + model.B.C = Constraint(expr=model.X == 1) + + opt._add_block(model.B) + + self.assertEqual(opt._solver_model.getNConss(), 1) + con = opt._solver_model.getConss()[0] + self.assertEqual(con.isLinear(), 1) + self.assertEqual(opt._solver_model.getRhs(con), 1) + + def test_add_block_containing_multiple_constraints(self): + model = ConcreteModel() + model.X = Var(within=Binary) + + opt = SolverFactory("scip_direct", solver_io="python") + opt._set_instance(model) + + self.assertEqual(opt._solver_model.getNConss(), 0) + + model.B = Block() + model.B.C1 = Constraint(expr=model.X == 1) + model.B.C2 = Constraint(expr=model.X <= 1) + model.B.C3 = Constraint(expr=model.X >= 1) + + opt._add_block(model.B) + + self.assertEqual(opt._solver_model.getNConss(), 3) + + +@unittest.skipIf(not scip_available, "The SCIP python bindings are not available") +class TestLoadVars(unittest.TestCase): + def setUp(self): + opt = SolverFactory("scip_direct", solver_io="python") + model = ConcreteModel() + model.X = Var(within=NonNegativeReals, initialize=0) + model.Y = Var(within=NonNegativeReals, initialize=0) + + model.C1 = Constraint(expr=2 * model.X + model.Y >= 8) + model.C2 = Constraint(expr=model.X + 3 * model.Y >= 6) + + model.O = Objective(expr=model.X + model.Y) + + opt.solve(model, load_solutions=False, save_results=False) + + self._model = model + self._opt = opt + + def test_all_vars_are_loaded(self): + self.assertTrue(self._model.X.stale) + self.assertTrue(self._model.Y.stale) + self.assertEqual(value(self._model.X), 0) + self.assertEqual(value(self._model.Y), 0) + + self._opt.load_vars() + + self.assertFalse(self._model.X.stale) + self.assertFalse(self._model.Y.stale) + self.assertAlmostEqual(value(self._model.X), 3.6) + self.assertAlmostEqual(value(self._model.Y), 0.8) + + def test_only_specified_vars_are_loaded(self): + self.assertTrue(self._model.X.stale) + self.assertTrue(self._model.Y.stale) + self.assertEqual(value(self._model.X), 0) + self.assertEqual(value(self._model.Y), 0) + + self._opt.load_vars([self._model.X]) + + self.assertFalse(self._model.X.stale) + self.assertTrue(self._model.Y.stale) + self.assertAlmostEqual(value(self._model.X), 3.6) + self.assertEqual(value(self._model.Y), 0) + + self._opt.load_vars([self._model.Y]) + + self.assertFalse(self._model.X.stale) + self.assertFalse(self._model.Y.stale) + self.assertAlmostEqual(value(self._model.X), 3.6) + self.assertAlmostEqual(value(self._model.Y), 0.8) + + +if __name__ == "__main__": + unittest.main() diff --git a/pyomo/solvers/tests/checks/test_SCIPPersistent.py b/pyomo/solvers/tests/checks/test_SCIPPersistent.py new file mode 100644 index 00000000000..0cf1aab65f6 --- /dev/null +++ b/pyomo/solvers/tests/checks/test_SCIPPersistent.py @@ -0,0 +1,318 @@ +# ___________________________________________________________________________ +# +# 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 pyomo.environ +import pyomo.common.unittest as unittest + +from pyomo.core import ( + ConcreteModel, + Var, + Objective, + Constraint, + NonNegativeReals, + NonNegativeIntegers, + Reals, + Binary, + SOSConstraint, + Set, + sin, + cos, + exp, + log, +) +from pyomo.opt import SolverFactory + +try: + import pyscipopt + + scip_available = True +except ImportError: + scip_available = False + + +@unittest.skipIf(not scip_available, "The SCIP python bindings are not available") +class TestQuadraticObjective(unittest.TestCase): + def test_quadratic_objective_linear_surrogate_is_set(self): + m = ConcreteModel() + m.X = Var(bounds=(-2, 2)) + m.Y = Var(bounds=(-2, 2)) + m.Z = Var(within=Reals) + m.O = Objective(expr=m.Z) + m.C1 = Constraint(expr=m.Y >= 2 * m.X - 1) + m.C2 = Constraint(expr=m.Y >= -m.X + 2) + m.C3 = Constraint(expr=m.Z >= m.X**2 + m.Y**2) + opt = SolverFactory("scip_persistent") + opt.set_instance(m) + opt.solve() + + self.assertAlmostEqual(m.X.value, 1, places=3) + self.assertAlmostEqual(m.Y.value, 1, places=3) + + opt.reset() + + opt.remove_constraint(m.C3) + del m.C3 + m.C3 = Constraint(expr=m.Z >= m.X**2) + opt.add_constraint(m.C3) + opt.solve() + self.assertAlmostEqual(m.X.value, 0, places=3) + self.assertAlmostEqual(m.Y.value, 2, places=3) + + def test_add_and_remove_sos(self): + m = ConcreteModel() + m.I = Set(initialize=[1, 2, 3]) + m.X = Var(m.I, bounds=(-2, 2)) + + m.C = SOSConstraint(var=m.X, sos=1) + + m.O = Objective(expr=m.X[1] + m.X[2]) + + opt = SolverFactory("scip_persistent") + + opt.set_instance(m) + opt.solve() + + zero_val_var = 0 + for i in range(1, 4): + if -0.001 < m.X[i].value < 0.001: + zero_val_var += 1 + assert zero_val_var == 2 + + opt.reset() + + opt.remove_sos_constraint(m.C) + del m.C + + m.C = SOSConstraint(var=m.X, sos=2) + opt.add_sos_constraint(m.C) + + opt.solve() + + zero_val_var = 0 + for i in range(1, 4): + if -0.001 < m.X[i].value < 0.001: + zero_val_var += 1 + assert zero_val_var == 1 + + def test_get_and_set_param(self): + m = ConcreteModel() + m.X = Var(bounds=(-2, 2)) + m.O = Objective(expr=m.X) + m.C3 = Constraint(expr=m.X <= 2) + opt = SolverFactory("scip_persistent") + opt.set_instance(m) + + opt.set_scip_param("limits/time", 60) + + assert opt.get_scip_param("limits/time") == 60 + + def test_non_linear(self): + + PI = 3.141592653589793238462643 + NWIRES = 11 + DIAMETERS = [ + 0.207, + 0.225, + 0.244, + 0.263, + 0.283, + 0.307, + 0.331, + 0.362, + 0.394, + 0.4375, + 0.500, + ] + PRELOAD = 300.0 + MAXWORKLOAD = 1000.0 + MAXDEFLECT = 6.0 + DEFLECTPRELOAD = 1.25 + MAXFREELEN = 14.0 + MAXCOILDIAM = 3.0 + MAXSHEARSTRESS = 189000.0 + SHEARMOD = 11500000.0 + + m = ConcreteModel() + m.coil = Var(within=NonNegativeReals) + m.wire = Var(within=NonNegativeReals) + m.defl = Var( + bounds=(DEFLECTPRELOAD / (MAXWORKLOAD - PRELOAD), MAXDEFLECT / PRELOAD) + ) + m.ncoils = Var(within=NonNegativeIntegers) + m.const1 = Var(within=NonNegativeReals) + m.const2 = Var(within=NonNegativeReals) + m.volume = Var(within=NonNegativeReals) + m.I = Set(initialize=[i for i in range(NWIRES)]) + m.y = Var(m.I, within=Binary) + + m.O = Objective(expr=m.volume) + + m.c1 = Constraint( + expr=PI / 2 * (m.ncoils + 2) * m.coil * m.wire**2 - m.volume == 0 + ) + + m.c2 = Constraint(expr=m.coil / m.wire - m.const1 == 0) + + m.c3 = Constraint( + expr=(4 * m.const1 - 1) / (4 * m.const1 - 4) + 0.615 / m.const1 - m.const2 + == 0 + ) + + m.c4 = Constraint( + expr=8.0 * MAXWORKLOAD / PI * m.const1 * m.const2 + - MAXSHEARSTRESS * m.wire**2 + <= 0 + ) + + m.c5 = Constraint( + expr=8 / SHEARMOD * m.ncoils * m.const1**3 / m.wire - m.defl == 0 + ) + + m.c6 = Constraint( + expr=MAXWORKLOAD * m.defl + 1.05 * m.ncoils * m.wire + 2.1 * m.wire + <= MAXFREELEN + ) + + m.c7 = Constraint(expr=m.coil + m.wire <= MAXCOILDIAM) + + m.c8 = Constraint( + expr=sum(m.y[i] * DIAMETERS[i] for i in range(NWIRES)) - m.wire == 0 + ) + + m.c9 = Constraint(expr=sum(m.y[i] for i in range(NWIRES)) == 1) + + opt = SolverFactory("scip_persistent") + opt.set_instance(m) + + opt.solve() + + self.assertAlmostEqual(m.volume.value, 1.6924910128, places=2) + + def test_non_linear_unary_expressions(self): + + m = ConcreteModel() + m.X = Var(bounds=(1, 2)) + m.Y = Var(within=Reals) + + m.O = Objective(expr=m.Y) + + m.C = Constraint(expr=exp(m.X) == m.Y) + + opt = SolverFactory("scip_persistent") + opt.set_instance(m) + + opt.solve() + self.assertAlmostEqual(m.X.value, 1, places=3) + self.assertAlmostEqual(m.Y.value, exp(1), places=3) + + opt.reset() + opt.remove_constraint(m.C) + del m.C + + m.C = Constraint(expr=log(m.X) == m.Y) + opt.add_constraint(m.C) + opt.solve() + self.assertAlmostEqual(m.X.value, 1, places=3) + self.assertAlmostEqual(m.Y.value, 0, places=3) + + opt.reset() + opt.remove_constraint(m.C) + del m.C + + m.C = Constraint(expr=sin(m.X) == m.Y) + opt.add_constraint(m.C) + opt.solve() + self.assertAlmostEqual(m.X.value, 1, places=3) + self.assertAlmostEqual(m.Y.value, sin(1), places=3) + + opt.reset() + opt.remove_constraint(m.C) + del m.C + + m.C = Constraint(expr=cos(m.X) == m.Y) + opt.add_constraint(m.C) + opt.solve() + self.assertAlmostEqual(m.X.value, 2, places=3) + self.assertAlmostEqual(m.Y.value, cos(2), places=3) + + def test_add_column(self): + m = ConcreteModel() + m.x = Var(within=NonNegativeReals) + m.c = Constraint(expr=(0, m.x, 1)) + m.obj = Objective(expr=-m.x) + + opt = SolverFactory("scip_persistent") + opt.set_instance(m) + opt.solve() + self.assertAlmostEqual(m.x.value, 1) + + m.y = Var(within=NonNegativeReals) + + opt.reset() + + opt.add_column(m, m.y, -3, [m.c], [2]) + opt.solve() + + self.assertAlmostEqual(m.x.value, 0) + self.assertAlmostEqual(m.y.value, 0.5) + + def test_add_column_exceptions(self): + m = ConcreteModel() + m.x = Var() + m.c = Constraint(expr=(0, m.x, 1)) + m.ci = Constraint([1, 2], rule=lambda m, i: (0, m.x, i + 1)) + m.cd = Constraint(expr=(0, -m.x, 1)) + m.cd.deactivate() + m.obj = Objective(expr=-m.x) + + opt = SolverFactory("scip_persistent") + + # set_instance not called + self.assertRaises(RuntimeError, opt.add_column, m, m.x, 0, [m.c], [1]) + + opt.set_instance(m) + + m2 = ConcreteModel() + m2.y = Var() + m2.c = Constraint(expr=(0, m.x, 1)) + + # different model than attached to opt + self.assertRaises(RuntimeError, opt.add_column, m2, m2.y, 0, [], []) + # pyomo var attached to different model + self.assertRaises(RuntimeError, opt.add_column, m, m2.y, 0, [], []) + + z = Var() + # pyomo var floating + self.assertRaises(RuntimeError, opt.add_column, m, z, -2, [m.c, z], [1]) + + m.y = Var() + # len(coefficients) == len(constraints) + self.assertRaises(RuntimeError, opt.add_column, m, m.y, -2, [m.c], [1, 2]) + self.assertRaises(RuntimeError, opt.add_column, m, m.y, -2, [m.c, z], [1]) + + # add indexed constraint + self.assertRaises(AttributeError, opt.add_column, m, m.y, -2, [m.ci], [1]) + # add something not a _ConstraintData + self.assertRaises(AttributeError, opt.add_column, m, m.y, -2, [m.x], [1]) + + # constraint not on solver model + self.assertRaises(KeyError, opt.add_column, m, m.y, -2, [m2.c], [1]) + + # inactive constraint + self.assertRaises(KeyError, opt.add_column, m, m.y, -2, [m.cd], [1]) + + opt.add_var(m.y) + # var already in solver model + self.assertRaises(RuntimeError, opt.add_column, m, m.y, -2, [m.c], [1]) + + +if __name__ == "__main__": + unittest.main() diff --git a/pyomo/solvers/tests/solvers.py b/pyomo/solvers/tests/solvers.py index 918a801ae37..3ad944de8d1 100644 --- a/pyomo/solvers/tests/solvers.py +++ b/pyomo/solvers/tests/solvers.py @@ -376,6 +376,27 @@ def test_solver_cases(*args): name='scip', io='nl', capabilities=_scip_capabilities, import_suffixes=[] ) + # + # SCIP PERSISTENT + # + + _scip_persistent_capabilities = set( + [ + "linear", + "integer", + "quadratic_constraint", + "sos1", + "sos2", + ] + ) + + _test_solver_cases["scip_persistent", "python"] = initialize( + name="scip_persistent", + io="python", + capabilities=_scip_persistent_capabilities, + import_suffixes=["slack", "dual", "rc"], + ) + # # CONOPT # From 6a14f108636dc9afb4e854b2fb27512aeb719ad0 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Mon, 18 Mar 2024 17:20:01 +0100 Subject: [PATCH 02/25] Add SCIPPersistent to docs --- doc/OnlineDocs/library_reference/solvers/index.rst | 1 + .../library_reference/solvers/scip_persistent.rst | 7 +++++++ 2 files changed, 8 insertions(+) create mode 100644 doc/OnlineDocs/library_reference/solvers/scip_persistent.rst diff --git a/doc/OnlineDocs/library_reference/solvers/index.rst b/doc/OnlineDocs/library_reference/solvers/index.rst index 400032df076..628f9cfdab0 100644 --- a/doc/OnlineDocs/library_reference/solvers/index.rst +++ b/doc/OnlineDocs/library_reference/solvers/index.rst @@ -9,3 +9,4 @@ Solver Interfaces gurobi_direct.rst gurobi_persistent.rst xpress_persistent.rst + scip_persistent.rst diff --git a/doc/OnlineDocs/library_reference/solvers/scip_persistent.rst b/doc/OnlineDocs/library_reference/solvers/scip_persistent.rst new file mode 100644 index 00000000000..63ed55b74e3 --- /dev/null +++ b/doc/OnlineDocs/library_reference/solvers/scip_persistent.rst @@ -0,0 +1,7 @@ +SCIPPersistent +================ + +.. autoclass:: pyomo.solvers.plugins.solvers.scip_persistent.SCIPPersistent + :members: + :inherited-members: + :show-inheritance: \ No newline at end of file From c1079090567bbe95b290402a3918c936a0ded576 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Tue, 19 Mar 2024 11:48:19 +0100 Subject: [PATCH 03/25] Add SCIp to Github action scripts --- .github/workflows/test_branches.yml | 6 ++++++ .github/workflows/test_pr_and_main.yml | 6 ++++++ pyomo/solvers/plugins/solvers/scip_persistent.py | 5 +++++ 3 files changed, 17 insertions(+) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 55f903a37f9..89e789db5ba 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -268,6 +268,12 @@ jobs: || echo "WARNING: Gurobi is not available" python -m pip install --cache-dir cache/pip xpress \ || echo "WARNING: Xpress Community Edition is not available" + if [[ ${{matrix.python}} == pypy* ]]; then + echo "skipping SCIP for pypy" + else + python -m pip install --cache-dir cache/pip pyscipopt==5.0.0 \ + || echo "WARNING: SCIP is not available" + fi if [[ ${{matrix.python}} == pypy* ]]; then echo "skipping wntr for pypy" else diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index 76ec6de951a..a6cf6ef7eec 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -298,6 +298,12 @@ jobs: || echo "WARNING: Gurobi is not available" python -m pip install --cache-dir cache/pip xpress \ || echo "WARNING: Xpress Community Edition is not available" + if [[ ${{matrix.python}} == pypy* ]]; then + echo "skipping SCIP for pypy" + else + python -m pip install --cache-dir cache/pip pyscipopt==5.0.0 \ + || echo "WARNING: SCIP is not available" + fi if [[ ${{matrix.python}} == pypy* ]]; then echo "skipping wntr for pypy" else diff --git a/pyomo/solvers/plugins/solvers/scip_persistent.py b/pyomo/solvers/plugins/solvers/scip_persistent.py index 408aa84633f..e28c91073ab 100644 --- a/pyomo/solvers/plugins/solvers/scip_persistent.py +++ b/pyomo/solvers/plugins/solvers/scip_persistent.py @@ -182,4 +182,9 @@ def _add_column(self, var, obj_coef, constraints, coefficients): self._solver_model.setObjective(obj_coef * scip_var, sense=sense, clear=False) def reset(self): + """ This function is necessary to call before making any changes to the + SCIP model after optimizing. It frees solution run specific information + that is not automatically done when changes to an already solved model + are made. Making changes to an already optimized model, e.g. adding additional + constraints will raise an error unless this function is called. """ self._solver_model.freeTransform() From e00ded8e33e823b8a9146facff85166291e17d71 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Tue, 19 Mar 2024 14:55:52 +0100 Subject: [PATCH 04/25] Remove 5.0.0 specific version. Add conda to workflow --- .github/workflows/test_branches.yml | 4 ++-- .github/workflows/test_pr_and_main.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test_branches.yml b/.github/workflows/test_branches.yml index 89e789db5ba..1d61aaf2d77 100644 --- a/.github/workflows/test_branches.yml +++ b/.github/workflows/test_branches.yml @@ -271,7 +271,7 @@ jobs: if [[ ${{matrix.python}} == pypy* ]]; then echo "skipping SCIP for pypy" else - python -m pip install --cache-dir cache/pip pyscipopt==5.0.0 \ + python -m pip install --cache-dir cache/pip pyscipopt \ || echo "WARNING: SCIP is not available" fi if [[ ${{matrix.python}} == pypy* ]]; then @@ -347,7 +347,7 @@ jobs: if test -z "${{matrix.slim}}"; then PYVER=$(echo "py${{matrix.python}}" | sed 's/\.//g') echo "Installing for $PYVER" - for PKG in 'cplex>=12.10' docplex 'gurobi=10.0.3' xpress cyipopt pymumps scip; do + for PKG in 'cplex>=12.10' docplex 'gurobi=10.0.3' xpress cyipopt pymumps scip pyscipopt; do echo "" echo "*** Install $PKG ***" # conda can literally take an hour to determine that a diff --git a/.github/workflows/test_pr_and_main.yml b/.github/workflows/test_pr_and_main.yml index a6cf6ef7eec..89fd90c41d0 100644 --- a/.github/workflows/test_pr_and_main.yml +++ b/.github/workflows/test_pr_and_main.yml @@ -301,7 +301,7 @@ jobs: if [[ ${{matrix.python}} == pypy* ]]; then echo "skipping SCIP for pypy" else - python -m pip install --cache-dir cache/pip pyscipopt==5.0.0 \ + python -m pip install --cache-dir cache/pip pyscipopt \ || echo "WARNING: SCIP is not available" fi if [[ ${{matrix.python}} == pypy* ]]; then @@ -376,7 +376,7 @@ jobs: if test -z "${{matrix.slim}}"; then PYVER=$(echo "py${{matrix.python}}" | sed 's/\.//g') echo "Installing for $PYVER" - for PKG in 'cplex>=12.10' docplex 'gurobi=10.0.3' xpress cyipopt pymumps scip; do + for PKG in 'cplex>=12.10' docplex 'gurobi=10.0.3' xpress cyipopt pymumps scip pyscipopt; do echo "" echo "*** Install $PKG ***" # conda can literally take an hour to determine that a From a0b625060217e04aa126dc9f2cb9a410e0968078 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Tue, 19 Mar 2024 15:14:54 +0100 Subject: [PATCH 05/25] Standardise string formatting to fstring --- pyomo/solvers/plugins/solvers/scip_direct.py | 35 +++++++------------ .../plugins/solvers/scip_persistent.py | 8 ++--- 2 files changed, 16 insertions(+), 27 deletions(-) diff --git a/pyomo/solvers/plugins/solvers/scip_direct.py b/pyomo/solvers/plugins/solvers/scip_direct.py index 0aafb596007..e93e5579f26 100644 --- a/pyomo/solvers/plugins/solvers/scip_direct.py +++ b/pyomo/solvers/plugins/solvers/scip_direct.py @@ -74,7 +74,7 @@ def _init(self): except ImportError: self._python_api_exists = False except Exception as e: - print("Import of pyscipopt failed - SCIP message=" + str(e) + "\n") + print(f"Import of pyscipopt failed - SCIP message={str(e)}\n") self._python_api_exists = False # Note: Undefined capabilities default to None @@ -104,7 +104,7 @@ def _apply_solver(self): if self._keepfiles: # Only save log file when the user wants to keep it. self._solver_model.setLogfile(self._log_file) - print("Solver log file: " + self._log_file) + print(f"Solver log file: {self._log_file}") # Set user specified parameters for key, option in self.options.items(): @@ -257,14 +257,14 @@ def __exit__(self, t, v, traceback): def _set_instance(self, model, kwds={}): DirectOrPersistentSolver._set_instance(self, model, kwds) + self.available() try: self._solver_model = self._scip.Model() except Exception: e = sys.exc_info()[1] msg = ( "Unable to create SCIP model. " - "Have you installed PySCIPOpt correctly?\n\n\t" - + "Error message: {0}".format(e) + f"Have you installed PySCIPOpt correctly?\n\n\t Error message: {e}" ) raise Exception(msg) @@ -275,14 +275,13 @@ def _set_instance(self, model, kwds={}): if var.fixed: if not self._output_fixed_variable_bounds: raise ValueError( - "Encountered a fixed variable (%s) inside " + f"Encountered a fixed variable {var.name} inside " "an active objective or constraint " - "expression on model %s, which is usually " + f"expression on model {self._pyomo_model.name}, which is usually " "indicative of a preprocessing error. Use " "the IO-option 'output_fixed_variable_bounds=True' " "to suppress this error and fix the variable " "by overwriting its bounds in the SCIP instance." - % (var.name, self._pyomo_model.name) ) def _add_block(self, block): @@ -308,14 +307,10 @@ def _add_constraint(self, con): if con.has_lb(): if not is_fixed(con.lower): - raise ValueError( - "Lower bound of constraint {0} is not constant.".format(con) - ) + raise ValueError(f"Lower bound of constraint {con} is not constant.") if con.has_ub(): if not is_fixed(con.upper): - raise ValueError( - "Upper bound of constraint {0} is not constant.".format(con) - ) + raise ValueError(f"Upper bound of constraint {con} is not constant.") if con.equality: scip_cons = self._solver_model.addCons( @@ -335,8 +330,7 @@ def _add_constraint(self, con): ) else: raise ValueError( - "Constraint does not have a lower " - "or an upper bound: {0} \n".format(con) + f"Constraint does not have a lower or an upper bound: {con} \n" ) for var in referenced_vars: @@ -398,9 +392,7 @@ def _scip_vtype_from_var(self, var): elif var.is_continuous(): vtype = "C" else: - raise ValueError( - "Variable domain type is not recognized for {0}".format(var.domain) - ) + raise ValueError(f"Variable domain type is not recognized for {var.domain}") return vtype def _set_objective(self, obj): @@ -418,7 +410,7 @@ def _set_objective(self, obj): elif obj.sense == maximize: sense = "maximize" else: - raise ValueError("Objective sense is not recognized: {0}".format(obj.sense)) + raise ValueError(f"Objective sense is not recognized: {obj.sense}") scip_expr, referenced_vars = self._get_expr_from_pyomo_expr( obj.expr, self._max_obj_degree @@ -455,8 +447,7 @@ def _postsolve(self): flag = True if not flag: raise RuntimeError( - "***The scip_direct solver plugin cannot extract solution suffix=" - + suffix + f"***The scip_direct solver plugin cannot extract solution suffix={suffix}" ) scip = self._solver_model @@ -593,7 +584,7 @@ def _postsolve(self): else: self.results.solver.status = SolverStatus.error self.results.solver.termination_message = ( - "Unhandled SCIP status (" + str(status) + ")" + f"Unhandled SCIP status ({str(status)})" ) self.results.solver.termination_condition = TerminationCondition.error soln.status = SolutionStatus.error diff --git a/pyomo/solvers/plugins/solvers/scip_persistent.py b/pyomo/solvers/plugins/solvers/scip_persistent.py index e28c91073ab..abb85b8dbca 100644 --- a/pyomo/solvers/plugins/solvers/scip_persistent.py +++ b/pyomo/solvers/plugins/solvers/scip_persistent.py @@ -84,9 +84,7 @@ def update_var(self, var): # return if var not in self._pyomo_var_to_solver_var_map: raise ValueError( - "The Var provided to compile_var needs to be added first: {0}".format( - var - ) + f"The Var provided to compile_var needs to be added first: {var}" ) scip_var = self._pyomo_var_to_solver_var_map[var] vtype = self._scip_vtype_from_var(var) @@ -182,9 +180,9 @@ def _add_column(self, var, obj_coef, constraints, coefficients): self._solver_model.setObjective(obj_coef * scip_var, sense=sense, clear=False) def reset(self): - """ This function is necessary to call before making any changes to the + """This function is necessary to call before making any changes to the SCIP model after optimizing. It frees solution run specific information that is not automatically done when changes to an already solved model are made. Making changes to an already optimized model, e.g. adding additional - constraints will raise an error unless this function is called. """ + constraints will raise an error unless this function is called.""" self._solver_model.freeTransform() From d0816eb008bae43eff5bff54390873c98e5b7a1b Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Tue, 19 Mar 2024 15:17:29 +0100 Subject: [PATCH 06/25] Add parameter link to docstring --- pyomo/solvers/plugins/solvers/scip_persistent.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyomo/solvers/plugins/solvers/scip_persistent.py b/pyomo/solvers/plugins/solvers/scip_persistent.py index abb85b8dbca..49fe224e72a 100644 --- a/pyomo/solvers/plugins/solvers/scip_persistent.py +++ b/pyomo/solvers/plugins/solvers/scip_persistent.py @@ -116,6 +116,7 @@ def set_scip_param(self, param, val): param: str The SCIP parameter to set. Options include any SCIP parameter. Please see the SCIP documentation for options. + Link at: https://www.scipopt.org/doc/html/PARAMETERS.php val: any The value to set the parameter to. See SCIP documentation for possible values. """ @@ -129,6 +130,7 @@ def get_scip_param(self, param): ---------- param: str or int or float The SCIP parameter to get the value of. See SCIP documentation for possible options. + Link at: https://www.scipopt.org/doc/html/PARAMETERS.php """ return self._solver_model.getParam(param) From 0e11f112161b6947ddf30d6530d6167ef174cf44 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Tue, 19 Mar 2024 15:20:07 +0100 Subject: [PATCH 07/25] Remove redundant second objective sense check --- pyomo/solvers/plugins/solvers/scip_direct.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/pyomo/solvers/plugins/solvers/scip_direct.py b/pyomo/solvers/plugins/solvers/scip_direct.py index e93e5579f26..c6285ff53cb 100644 --- a/pyomo/solvers/plugins/solvers/scip_direct.py +++ b/pyomo/solvers/plugins/solvers/scip_direct.py @@ -591,15 +591,6 @@ def _postsolve(self): self.results.problem.name = scip.getProbName() - if scip.getObjectiveSense() == "minimize": - self.results.problem.sense = minimize - elif scip.getObjectiveSense() == "maximize": - self.results.problem.sense = maximize - else: - raise RuntimeError( - f"Unrecognized SCIP objective sense: {scip.getObjectiveSense()}" - ) - self.results.problem.upper_bound = None self.results.problem.lower_bound = None if scip.getNSols() > 0: From 068ec99277321743b611c5323be3858a5b505ce8 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Tue, 19 Mar 2024 15:35:54 +0100 Subject: [PATCH 08/25] Clean up _post_solve with a helper function for status handling --- pyomo/solvers/plugins/solvers/scip_direct.py | 109 +++++++++++-------- 1 file changed, 63 insertions(+), 46 deletions(-) diff --git a/pyomo/solvers/plugins/solvers/scip_direct.py b/pyomo/solvers/plugins/solvers/scip_direct.py index c6285ff53cb..9074d40870f 100644 --- a/pyomo/solvers/plugins/solvers/scip_direct.py +++ b/pyomo/solvers/plugins/solvers/scip_direct.py @@ -382,8 +382,16 @@ def _add_sos_constraint(self, con): def _scip_vtype_from_var(self, var): """ This function takes a pyomo variable and returns the appropriate SCIP variable type - :param var: pyomo.core.base.var.Var - :return: B, I, or C + + Parameters + ---------- + var: pyomo.core.base.var.Var + The pyomo variable that we want to retrieve the SCIP vtype of + + Returns + ------- + vtype: str + B for Binary, I for Integer, or C for Continuous """ if var.is_binary(): vtype = "B" @@ -425,52 +433,12 @@ def _set_objective(self, obj): self._needs_updated = True - def _postsolve(self): - # the only suffixes that we extract from SCIP are - # constraint duals, constraint slacks, and variable - # reduced-costs. scan through the solver suffix list - # and throw an exception if the user has specified - # any others. - extract_duals = False - extract_slacks = False - extract_reduced_costs = False - for suffix in self._suffixes: - flag = False - if re.match(suffix, "dual"): - extract_duals = True - flag = True - if re.match(suffix, "slack"): - extract_slacks = True - flag = True - if re.match(suffix, "rc"): - extract_reduced_costs = True - flag = True - if not flag: - raise RuntimeError( - f"***The scip_direct solver plugin cannot extract solution suffix={suffix}" - ) - - scip = self._solver_model + def _get_solver_solution_status(self, scip, soln): + """ """ + # Get the status of the SCIP Model currently status = scip.getStatus() - scip_vars = scip.getVars() - n_bin_vars = sum([scip_var.vtype() == "BINARY" for scip_var in scip_vars]) - n_int_vars = sum([scip_var.vtype() == "INTEGER" for scip_var in scip_vars]) - n_con_vars = sum([scip_var.vtype() == "CONTINUOUS" for scip_var in scip_vars]) - - if n_bin_vars + n_int_vars > 0: - if extract_reduced_costs: - logger.warning("Cannot get reduced costs for MIP.") - if extract_duals: - logger.warning("Cannot get duals for MIP.") - extract_reduced_costs = False - extract_duals = False - - self.results = SolverResults() - soln = Solution() - - self.results.solver.name = f"SCIP{self._version}" - self.results.solver.wallclock_time = scip.getSolvingTime() + # Go through each potential case and update appropriately if scip.getStage() == 1: # SCIP Model is created but not yet optimized self.results.solver.status = SolverStatus.aborted self.results.solver.termination_message = ( @@ -588,6 +556,55 @@ def _postsolve(self): ) self.results.solver.termination_condition = TerminationCondition.error soln.status = SolutionStatus.error + return soln + + def _postsolve(self): + # the only suffixes that we extract from SCIP are + # constraint duals, constraint slacks, and variable + # reduced-costs. scan through the solver suffix list + # and throw an exception if the user has specified + # any others. + extract_duals = False + extract_slacks = False + extract_reduced_costs = False + for suffix in self._suffixes: + flag = False + if re.match(suffix, "dual"): + extract_duals = True + flag = True + if re.match(suffix, "slack"): + extract_slacks = True + flag = True + if re.match(suffix, "rc"): + extract_reduced_costs = True + flag = True + if not flag: + raise RuntimeError( + f"***The scip_direct solver plugin cannot extract solution suffix={suffix}" + ) + + scip = self._solver_model + status = scip.getStatus() + scip_vars = scip.getVars() + n_bin_vars = sum([scip_var.vtype() == "BINARY" for scip_var in scip_vars]) + n_int_vars = sum([scip_var.vtype() == "INTEGER" for scip_var in scip_vars]) + n_con_vars = sum([scip_var.vtype() == "CONTINUOUS" for scip_var in scip_vars]) + + if n_bin_vars + n_int_vars > 0: + if extract_reduced_costs: + logger.warning("Cannot get reduced costs for MIP.") + if extract_duals: + logger.warning("Cannot get duals for MIP.") + extract_reduced_costs = False + extract_duals = False + + self.results = SolverResults() + soln = Solution() + + self.results.solver.name = f"SCIP{self._version}" + self.results.solver.wallclock_time = scip.getSolvingTime() + + soln = self._get_solver_solution_status(scip, soln) self.results.problem.name = scip.getProbName() From 63af6d8ce13a28b50a59c3d9a027d57db64d8ca6 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Tue, 19 Mar 2024 15:38:45 +0100 Subject: [PATCH 09/25] Remove individual skip_test option --- pyomo/solvers/tests/checks/test_SCIPDirect.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/pyomo/solvers/tests/checks/test_SCIPDirect.py b/pyomo/solvers/tests/checks/test_SCIPDirect.py index ee37f5ddcc8..cc9e114fed1 100644 --- a/pyomo/solvers/tests/checks/test_SCIPDirect.py +++ b/pyomo/solvers/tests/checks/test_SCIPDirect.py @@ -37,6 +37,7 @@ scip_available = False +@unittest.skipIf(not scip_available, "The SCIP python bindings are not available") class SCIPDirectTests(unittest.TestCase): def setUp(self): self.stderr = sys.stderr @@ -45,7 +46,6 @@ def setUp(self): def tearDown(self): sys.stderr = self.stderr - @unittest.skipIf(not scip_available, "The SCIP python bindings are not available") def test_infeasible_lp(self): with SolverFactory("scip_direct", solver_io="python") as opt: model = ConcreteModel() @@ -60,7 +60,6 @@ def test_infeasible_lp(self): results.solver.termination_condition, TerminationCondition.infeasible ) - @unittest.skipIf(not scip_available, "The SCIP python bindings are not available") def test_unbounded_lp(self): with SolverFactory("scip_direct", solver_io="python") as opt: model = ConcreteModel() @@ -77,7 +76,6 @@ def test_unbounded_lp(self): ), ) - @unittest.skipIf(not scip_available, "The SCIP python bindings are not available") def test_optimal_lp(self): with SolverFactory("scip_direct", solver_io="python") as opt: model = ConcreteModel() @@ -88,7 +86,6 @@ def test_optimal_lp(self): self.assertEqual(results.solution.status, SolutionStatus.optimal) - @unittest.skipIf(not scip_available, "The SCIP python bindings are not available") def test_get_duals_lp(self): with SolverFactory("scip_direct", solver_io="python") as opt: model = ConcreteModel() @@ -108,7 +105,6 @@ def test_get_duals_lp(self): self.assertAlmostEqual(model.dual[model.C1], 0.4) self.assertAlmostEqual(model.dual[model.C2], 0.2) - @unittest.skipIf(not scip_available, "The SCIP python bindings are not available") def test_infeasible_mip(self): with SolverFactory("scip_direct", solver_io="python") as opt: model = ConcreteModel() @@ -123,7 +119,6 @@ def test_infeasible_mip(self): results.solver.termination_condition, TerminationCondition.infeasible ) - @unittest.skipIf(not scip_available, "The SCIP python bindings are not available") def test_unbounded_mip(self): with SolverFactory("scip_direct", solver_io="python") as opt: model = AbstractModel() @@ -141,7 +136,6 @@ def test_unbounded_mip(self): ), ) - @unittest.skipIf(not scip_available, "The SCIP python bindings are not available") def test_optimal_mip(self): with SolverFactory("scip_direct", solver_io="python") as opt: model = ConcreteModel() From 4075d9ad8ed1200c3d06e74ca5d3a678dd4e7239 Mon Sep 17 00:00:00 2001 From: Mark Turner <64978342+Opt-Mucca@users.noreply.github.com> Date: Tue, 19 Mar 2024 15:48:02 +0100 Subject: [PATCH 10/25] Update pyomo/solvers/plugins/solvers/scip_persistent.py Co-authored-by: Miranda Mundt <55767766+mrmundt@users.noreply.github.com> --- pyomo/solvers/plugins/solvers/scip_persistent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyomo/solvers/plugins/solvers/scip_persistent.py b/pyomo/solvers/plugins/solvers/scip_persistent.py index 49fe224e72a..572a1b638e0 100644 --- a/pyomo/solvers/plugins/solvers/scip_persistent.py +++ b/pyomo/solvers/plugins/solvers/scip_persistent.py @@ -96,7 +96,7 @@ def update_var(self, var): def write(self, filename, filetype=""): """ - Write the model to a file (e.g., and lp file). + Write the model to a file (e.g., an lp file). Parameters ---------- From f55fcc5aaa329049a6ac521738537fb8498f1a36 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Tue, 19 Mar 2024 15:52:10 +0100 Subject: [PATCH 11/25] Update from the black command --- pyomo/solvers/tests/solvers.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/pyomo/solvers/tests/solvers.py b/pyomo/solvers/tests/solvers.py index 3ad944de8d1..1a5c1671f19 100644 --- a/pyomo/solvers/tests/solvers.py +++ b/pyomo/solvers/tests/solvers.py @@ -381,13 +381,7 @@ def test_solver_cases(*args): # _scip_persistent_capabilities = set( - [ - "linear", - "integer", - "quadratic_constraint", - "sos1", - "sos2", - ] + ["linear", "integer", "quadratic_constraint", "sos1", "sos2"] ) _test_solver_cases["scip_persistent", "python"] = initialize( From 91eae7b573624fe35e5fb579debde19efbed740a Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Tue, 19 Mar 2024 16:37:13 +0100 Subject: [PATCH 12/25] Fix typos --- pyomo/solvers/plugins/solvers/scip_direct.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyomo/solvers/plugins/solvers/scip_direct.py b/pyomo/solvers/plugins/solvers/scip_direct.py index 9074d40870f..5ba3395d1d2 100644 --- a/pyomo/solvers/plugins/solvers/scip_direct.py +++ b/pyomo/solvers/plugins/solvers/scip_direct.py @@ -94,7 +94,7 @@ def _init(self): def _apply_solver(self): StaleFlagManager.mark_all_as_stale() - # Supress solver output if requested + # Suppress solver output if requested if self._tee: self._solver_model.hideOutput(quiet=False) else: @@ -179,7 +179,7 @@ def get_nl_expr_recursively(pyomo_expr): elif isinstance(pyomo_expr, DivisionExpression): if len(scip_expr_list) != 2: raise ValueError( - f"DivisonExpression has {len(scip_expr_list)} many terms instead of two!" + f"DivisionExpression has {len(scip_expr_list)} many terms instead of two!" ) return scip_expr_list[0] / scip_expr_list[1] elif isinstance(pyomo_expr, UnaryFunctionExpression): From 5ee2007a05921ef90a5ef9b31e76a877c9899007 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Tue, 19 Mar 2024 17:28:07 +0100 Subject: [PATCH 13/25] Replace trySol via more safe checkSol --- pyomo/solvers/plugins/solvers/scip_direct.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/pyomo/solvers/plugins/solvers/scip_direct.py b/pyomo/solvers/plugins/solvers/scip_direct.py index 5ba3395d1d2..25c668a0a06 100644 --- a/pyomo/solvers/plugins/solvers/scip_direct.py +++ b/pyomo/solvers/plugins/solvers/scip_direct.py @@ -729,7 +729,14 @@ def _warm_start(self): for pyomo_var, scip_var in self._pyomo_var_to_solver_var_expr_map.items(): if pyomo_var.value is not None: scip_sol[scip_var] = value(pyomo_var) - self._solver_model.trySol(scip_sol, free=True) + feasible = self._solver_model.checkSol(scip_sol) + if feasible: + self._solver_model.addSol(scip_sol) + del scip_sol + else: + logger.warning("Warm start solution was not accepted by SCIP") + self._solver_model.freeSol(scip_sol) + del scip_sol def _load_vars(self, vars_to_load=None): var_map = self._pyomo_var_to_solver_var_expr_map From f6ff0923ba9dbbc2bfd04990846c1446ca9e9ed8 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Wed, 20 Mar 2024 10:01:01 +0100 Subject: [PATCH 14/25] Adds support for partial solution loading --- pyomo/solvers/plugins/solvers/scip_direct.py | 24 +++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/pyomo/solvers/plugins/solvers/scip_direct.py b/pyomo/solvers/plugins/solvers/scip_direct.py index 25c668a0a06..0d4ad722459 100644 --- a/pyomo/solvers/plugins/solvers/scip_direct.py +++ b/pyomo/solvers/plugins/solvers/scip_direct.py @@ -725,18 +725,30 @@ def warm_start_capable(self): return True def _warm_start(self): - scip_sol = self._solver_model.createSol() + partial_sol = False + for pyomo_var in self._pyomo_var_to_solver_var_expr_map: + if pyomo_var.value is None: + partial_sol = True + break + if partial_sol: + scip_sol = self._solver_model.createPartialSol() + else: + scip_sol = self._solver_model.createSol() for pyomo_var, scip_var in self._pyomo_var_to_solver_var_expr_map.items(): if pyomo_var.value is not None: scip_sol[scip_var] = value(pyomo_var) - feasible = self._solver_model.checkSol(scip_sol) - if feasible: + if partial_sol: self._solver_model.addSol(scip_sol) del scip_sol else: - logger.warning("Warm start solution was not accepted by SCIP") - self._solver_model.freeSol(scip_sol) - del scip_sol + feasible = self._solver_model.checkSol(scip_sol) + if feasible: + self._solver_model.addSol(scip_sol) + del scip_sol + else: + logger.warning("Warm start solution was not accepted by SCIP") + self._solver_model.freeSol(scip_sol) + del scip_sol def _load_vars(self, vars_to_load=None): var_map = self._pyomo_var_to_solver_var_expr_map From e7ac980a6e58a9b1035bafc38cf3ee55420900b8 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Thu, 21 Mar 2024 09:58:24 +0100 Subject: [PATCH 15/25] Add error handling for setting non-linear objective --- pyomo/solvers/plugins/solvers/scip_direct.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pyomo/solvers/plugins/solvers/scip_direct.py b/pyomo/solvers/plugins/solvers/scip_direct.py index 0d4ad722459..456b370eff1 100644 --- a/pyomo/solvers/plugins/solvers/scip_direct.py +++ b/pyomo/solvers/plugins/solvers/scip_direct.py @@ -134,6 +134,15 @@ def _apply_solver(self): def _get_expr_from_pyomo_repn(self, repn, max_degree=None): referenced_vars = ComponentSet() + degree = repn.polynomial_degree() + if (max_degree is not None) and (degree > max_degree): + raise DegreeError( + "While SCIP supports general non-linear constraints, the objective must be linear. " + "Please reformulate the objective by introducing a new variable. " + "For min problems: min z s.t z >= f(x). For max problems: max z s.t z <= f(x). " + "f(x) is the original non-linear objective." + ) + new_expr = repn.constant if len(repn.linear_vars) > 0: From 2540f650df319bcca59cf26d0bc524a7fab7de8c Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Fri, 19 Apr 2024 18:44:29 +0200 Subject: [PATCH 16/25] Remove dual and rc loading for SCIP. Fix bug of ranged rows --- pyomo/solvers/plugins/solvers/scip_direct.py | 151 ++++++------------ .../plugins/solvers/scip_persistent.py | 21 ++- pyomo/solvers/tests/solvers.py | 2 +- 3 files changed, 73 insertions(+), 101 deletions(-) diff --git a/pyomo/solvers/plugins/solvers/scip_direct.py b/pyomo/solvers/plugins/solvers/scip_direct.py index 456b370eff1..04440b59f9b 100644 --- a/pyomo/solvers/plugins/solvers/scip_direct.py +++ b/pyomo/solvers/plugins/solvers/scip_direct.py @@ -86,10 +86,12 @@ def _init(self): self._capabilities.integer = True self._capabilities.sos1 = True self._capabilities.sos2 = True + self._skip_trivial_constraints = True # Dictionary used exclusively for SCIP, as we want the constraint expressions self._pyomo_var_to_solver_var_expr_map = ComponentMap() self._pyomo_con_to_solver_con_expr_map = dict() + self._pyomo_con_to_solver_expr_map = dict() def _apply_solver(self): StaleFlagManager.mark_all_as_stale() @@ -239,6 +241,7 @@ def _scip_lb_ub_from_var(self, var): ub = value(var.ub) else: ub = self._solver_model.infinity() + return lb, ub def _add_var(self, var): @@ -327,7 +330,10 @@ def _add_constraint(self, con): ) elif con.has_lb() and con.has_ub(): scip_cons = self._solver_model.addCons( - value(con.lower) <= (scip_expr <= value(con.upper)), name=conname + value(con.lower) <= scip_expr, name=conname + ) + self._solver_model.chgRhs( + scip_cons, value(con.upper) - value(con.body.constant) ) elif con.has_lb(): scip_cons = self._solver_model.addCons( @@ -346,6 +352,7 @@ def _add_constraint(self, con): self._referenced_variables[var] += 1 self._vars_referenced_by_con[con] = referenced_vars self._pyomo_con_to_solver_con_expr_map[con] = scip_cons + self._pyomo_con_to_solver_expr_map[con] = scip_expr self._pyomo_con_to_solver_con_map[con] = scip_cons.name self._solver_con_to_pyomo_con_map[conname] = con @@ -440,8 +447,6 @@ def _set_objective(self, obj): self._objective = obj self._vars_referenced_by_obj = referenced_vars - self._needs_updated = True - def _get_solver_solution_status(self, scip, soln): """ """ # Get the status of the SCIP Model currently @@ -569,24 +574,17 @@ def _get_solver_solution_status(self, scip, soln): def _postsolve(self): # the only suffixes that we extract from SCIP are - # constraint duals, constraint slacks, and variable - # reduced-costs. scan through the solver suffix list + # constraint slacks. constraint duals and variable + # reduced-costs were removed as in SCIP they contain + # too many caveats. scan through the solver suffix list # and throw an exception if the user has specified # any others. - extract_duals = False extract_slacks = False - extract_reduced_costs = False for suffix in self._suffixes: flag = False - if re.match(suffix, "dual"): - extract_duals = True - flag = True if re.match(suffix, "slack"): extract_slacks = True flag = True - if re.match(suffix, "rc"): - extract_reduced_costs = True - flag = True if not flag: raise RuntimeError( f"***The scip_direct solver plugin cannot extract solution suffix={suffix}" @@ -599,14 +597,6 @@ def _postsolve(self): n_int_vars = sum([scip_var.vtype() == "INTEGER" for scip_var in scip_vars]) n_con_vars = sum([scip_var.vtype() == "CONTINUOUS" for scip_var in scip_vars]) - if n_bin_vars + n_int_vars > 0: - if extract_reduced_costs: - logger.warning("Cannot get reduced costs for MIP.") - if extract_duals: - logger.warning("Cannot get duals for MIP.") - extract_reduced_costs = False - extract_duals = False - self.results = SolverResults() soln = Solution() @@ -667,6 +657,7 @@ def _postsolve(self): This code in this if statement is only needed for backwards compatibility. It is more efficient to set _save_results to False and use load_vars, load_duals, etc. """ + if scip.getNSols() > 0: soln_variables = soln.variable soln_constraints = soln.constraint @@ -683,42 +674,35 @@ def _postsolve(self): if self._referenced_variables[pyomo_var] > 0: soln_variables[name] = {"Value": val} - if extract_reduced_costs: - vals = [scip.getVarRedcost(scip_var) for scip_var in scip_vars] - for scip_var, val, name in zip(scip_vars, vals, scip_var_names): - pyomo_var = self._solver_var_to_pyomo_var_map[name] - if self._referenced_variables[pyomo_var] > 0: - soln_variables[name]["Rc"] = val - - if extract_duals or extract_slacks: - scip_cons = scip.getConss() - con_names = [cons.name for cons in scip_cons] - assert set(self._solver_con_to_pyomo_con_map.keys()) == set( - con_names - ) - for name in con_names: - soln_constraints[name] = {} - - if extract_duals: - vals = [scip.getDualSolVal(con) for con in scip_cons] - for val, name in zip(vals, con_names): - soln_constraints[name]["Dual"] = val - if extract_slacks: - vals = [scip.getSlack(con, scip_sol) for con in scip_cons] - for val, name in zip(vals, con_names): - soln_constraints[name]["Slack"] = val + scip_cons = list(self._pyomo_con_to_solver_con_expr_map.values()) + con_names = [cons.name for cons in scip_cons] + if set(self._solver_con_to_pyomo_con_map.keys()) != set(con_names): + raise AssertionError( + f"{set(self._solver_con_to_pyomo_con_map.keys())}, {set(con_names)}" + ) + for cons in scip_cons: + if cons.getConshdlrName() in ["linear", "nonlinear"]: + soln_constraints[cons.name] = {} + pyomo_con = self._solver_con_to_pyomo_con_map[cons.name] + scip_expr = self._pyomo_con_to_solver_expr_map[pyomo_con] + activity = scip_sol[scip_expr] + if pyomo_con.has_lb(): + lhs = value(pyomo_con.lower) + else: + lhs = -1e20 + if pyomo_con.has_ub(): + rhs = value(pyomo_con.upper) + else: + rhs = 1e20 + soln_constraints[cons.name]["Slack"] = min( + activity - lhs, rhs - activity + ) elif self._load_solutions: if scip.getNSols() > 0: self.load_vars() - if extract_reduced_costs: - self._load_rc() - - if extract_duals: - self._load_duals() - if extract_slacks: self._load_slacks() @@ -773,65 +757,36 @@ def _load_vars(self, vars_to_load=None): var.set_value(val, skip_validation=True) def _load_rc(self, vars_to_load=None): - if not hasattr(self._pyomo_model, "rc"): - self._pyomo_model.rc = Suffix(direction=Suffix.IMPORT) - var_map = self._pyomo_var_to_solver_var_expr_map - ref_vars = self._referenced_variables - rc = self._pyomo_model.rc - if vars_to_load is None: - vars_to_load = var_map.keys() - - scip_vars_to_load = [var_map[pyomo_var] for pyomo_var in vars_to_load] - vals = [ - self._solver_model.getVarRedcost(scip_var) for scip_var in scip_vars_to_load - ] - - for var, val in zip(vars_to_load, vals): - if ref_vars[var] > 0: - rc[var] = val + raise NotImplementedError( + "SCIP via Pyomo does not support reduced cost loading." + ) def _load_duals(self, cons_to_load=None): - if not hasattr(self._pyomo_model, "dual"): - self._pyomo_model.dual = Suffix(direction=Suffix.IMPORT) - con_map = self._pyomo_con_to_solver_con_map - reverse_con_map = self._solver_con_to_pyomo_con_map - dual = self._pyomo_model.dual - scip_cons = self._solver_model.getConss() - - if cons_to_load is None: - con_names = [con.name for con in scip_cons] - vals = [self._solver_model.getDualSolVal(con) for con in scip_cons] - else: - con_names = set([con_map[pyomo_con] for pyomo_con in cons_to_load]) - scip_cons_to_load = [con for con in scip_cons if con.name in con_names] - vals = [self._solver_model.getDualSolVal(con) for con in scip_cons_to_load] - - for i, con_name in enumerate(con_names): - pyomo_con = reverse_con_map[con_name] - dual[pyomo_con] = vals[i] + raise NotImplementedError( + "SCIP via Pyomo does not support dual solution loading" + ) def _load_slacks(self, cons_to_load=None): if not hasattr(self._pyomo_model, "slack"): self._pyomo_model.slack = Suffix(direction=Suffix.IMPORT) - con_map = self._pyomo_con_to_solver_con_map - reverse_con_map = self._solver_con_to_pyomo_con_map slack = self._pyomo_model.slack - scip_cons = self._solver_model.getConss() scip_sol = self._solver_model.getBestSol() if cons_to_load is None: - con_names = [con.name for con in scip_cons] - vals = [self._solver_model.getSlack(con, scip_sol) for con in scip_cons] + scip_cons = list(self._pyomo_con_to_solver_con_expr_map.values()) else: - con_names = set([con_map[pyomo_con] for pyomo_con in cons_to_load]) - scip_cons_to_load = [con for con in scip_cons if con.name in con_names] - vals = [ - self._solver_model.getSlack(con, scip_sol) for con in scip_cons_to_load + scip_cons = [ + self._pyomo_con_to_solver_con_expr_map[pyomo_cons] + for pyomo_cons in cons_to_load ] - - for i, con_name in enumerate(con_names): - pyomo_con = reverse_con_map[con_name] - slack[pyomo_con] = vals[i] + for cons in scip_cons: + if cons.getConshdlrName() in ["linear", "nonlinear"]: + pyomo_con = self._solver_con_to_pyomo_con_map[cons.name] + scip_expr = self._pyomo_con_to_solver_expr_map[pyomo_con] + activity = scip_sol[scip_expr] + rhs = self._solver_model.getRhs(cons) + lhs = self._solver_model.getLhs(cons) + slack[pyomo_con] = min(activity - lhs, rhs - activity) def load_duals(self, cons_to_load=None): """ diff --git a/pyomo/solvers/plugins/solvers/scip_persistent.py b/pyomo/solvers/plugins/solvers/scip_persistent.py index 572a1b638e0..880380ced1f 100644 --- a/pyomo/solvers/plugins/solvers/scip_persistent.py +++ b/pyomo/solvers/plugins/solvers/scip_persistent.py @@ -8,7 +8,6 @@ # rights in this software. # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ - from pyomo.solvers.plugins.solvers.scip_direct import SCIPDirect from pyomo.solvers.plugins.solvers.persistent_solver import PersistentSolver from pyomo.opt.base import SolverFactory @@ -50,16 +49,34 @@ def _remove_constraint(self, solver_conname): con = self._solver_con_to_pyomo_con_map[solver_conname] scip_con = self._pyomo_con_to_solver_con_expr_map[con] self._solver_model.delCons(scip_con) - + for var in self._vars_reference_by_con[con]: + self._references_vars[var] -= 1 + del self._vars_reference_by_con[con] + del self._pyomo_con_to_solver_con_map[con] + del self._pyomo_con_to_solver_con_expr_map[con] + del self._pyomo_con_to_solver_expr_map[con] + del self._solver_con_to_pyomo_con_map[solver_conname] + + def _remove_sos_constraint(self, solver_sos_conname): con = self._solver_con_to_pyomo_con_map[solver_sos_conname] scip_con = self._pyomo_con_to_solver_con_expr_map[con] self._solver_model.delCons(scip_con) + for var in self._vars_reference_by_con[con]: + self._references_vars[var] -= 1 + del self._vars_reference_by_con[con] + del self._pyomo_con_to_solver_con_map[con] + del self._pyomo_con_to_solver_con_expr_map[con] + del self._solver_con_to_pyomo_con_map[solver_conname] def _remove_var(self, solver_varname): var = self._solver_var_to_pyomo_var_map[solver_varname] scip_var = self._pyomo_var_to_solver_var_expr_map[var] self._solver_model.delVar(scip_var) + del self._pyomo_var_to_solver_var_expr_map[var] + del self._pyomo_var_to_solver_var_map[var] + del self._solver_var_to_pyomo_var_map[scip_var.name] + del self._referenced_variables[var] def _warm_start(self): SCIPDirect._warm_start(self) diff --git a/pyomo/solvers/tests/solvers.py b/pyomo/solvers/tests/solvers.py index 1a5c1671f19..b66c1ca5af5 100644 --- a/pyomo/solvers/tests/solvers.py +++ b/pyomo/solvers/tests/solvers.py @@ -388,7 +388,7 @@ def test_solver_cases(*args): name="scip_persistent", io="python", capabilities=_scip_persistent_capabilities, - import_suffixes=["slack", "dual", "rc"], + import_suffixes=["slack"], ) # From 9e5d9442ea0900d36629a3f0677eb6c6ce8d7f19 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Fri, 19 Apr 2024 18:48:34 +0200 Subject: [PATCH 17/25] Add safe con.body.constant check --- pyomo/solvers/plugins/solvers/scip_direct.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pyomo/solvers/plugins/solvers/scip_direct.py b/pyomo/solvers/plugins/solvers/scip_direct.py index 04440b59f9b..1b5e81db302 100644 --- a/pyomo/solvers/plugins/solvers/scip_direct.py +++ b/pyomo/solvers/plugins/solvers/scip_direct.py @@ -332,9 +332,10 @@ def _add_constraint(self, con): scip_cons = self._solver_model.addCons( value(con.lower) <= scip_expr, name=conname ) - self._solver_model.chgRhs( - scip_cons, value(con.upper) - value(con.body.constant) - ) + rhs = value(con.upper) + if hasattr(con.body, "constant"): + rhs -= value(con.body.constant) + self._solver_model.chgRhs(scip_cons, rhs) elif con.has_lb(): scip_cons = self._solver_model.addCons( value(con.lower) <= scip_expr, name=conname From f90dfade88dafd2d150409efd0a216c34578c89d Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Mon, 29 Apr 2024 10:33:26 +0200 Subject: [PATCH 18/25] Remove slack loading for SCIP --- pyomo/solvers/plugins/solvers/scip_direct.py | 72 +++---------------- .../plugins/solvers/scip_persistent.py | 15 ---- 2 files changed, 8 insertions(+), 79 deletions(-) diff --git a/pyomo/solvers/plugins/solvers/scip_direct.py b/pyomo/solvers/plugins/solvers/scip_direct.py index 1b5e81db302..57cfc213f3d 100644 --- a/pyomo/solvers/plugins/solvers/scip_direct.py +++ b/pyomo/solvers/plugins/solvers/scip_direct.py @@ -91,7 +91,6 @@ def _init(self): # Dictionary used exclusively for SCIP, as we want the constraint expressions self._pyomo_var_to_solver_var_expr_map = ComponentMap() self._pyomo_con_to_solver_con_expr_map = dict() - self._pyomo_con_to_solver_expr_map = dict() def _apply_solver(self): StaleFlagManager.mark_all_as_stale() @@ -353,7 +352,6 @@ def _add_constraint(self, con): self._referenced_variables[var] += 1 self._vars_referenced_by_con[con] = referenced_vars self._pyomo_con_to_solver_con_expr_map[con] = scip_cons - self._pyomo_con_to_solver_expr_map[con] = scip_expr self._pyomo_con_to_solver_con_map[con] = scip_cons.name self._solver_con_to_pyomo_con_map[conname] = con @@ -574,22 +572,17 @@ def _get_solver_solution_status(self, scip, soln): return soln def _postsolve(self): - # the only suffixes that we extract from SCIP are - # constraint slacks. constraint duals and variable + # Constraint duals and variable # reduced-costs were removed as in SCIP they contain - # too many caveats. scan through the solver suffix list + # too many caveats. Slacks were removed as later + # planned interfaces do not intend to support. + # Scan through the solver suffix list # and throw an exception if the user has specified # any others. - extract_slacks = False for suffix in self._suffixes: - flag = False - if re.match(suffix, "slack"): - extract_slacks = True - flag = True - if not flag: - raise RuntimeError( - f"***The scip_direct solver plugin cannot extract solution suffix={suffix}" - ) + raise RuntimeError( + f"***The scip_direct solver plugin cannot extract solution suffix={suffix}" + ) scip = self._solver_model status = scip.getStatus() @@ -661,8 +654,6 @@ def _postsolve(self): if scip.getNSols() > 0: soln_variables = soln.variable - soln_constraints = soln.constraint - scip_sol = scip.getBestSol() scip_vars = scip.getVars() scip_var_names = [scip_var.name for scip_var in scip_vars] @@ -675,38 +666,10 @@ def _postsolve(self): if self._referenced_variables[pyomo_var] > 0: soln_variables[name] = {"Value": val} - if extract_slacks: - scip_cons = list(self._pyomo_con_to_solver_con_expr_map.values()) - con_names = [cons.name for cons in scip_cons] - if set(self._solver_con_to_pyomo_con_map.keys()) != set(con_names): - raise AssertionError( - f"{set(self._solver_con_to_pyomo_con_map.keys())}, {set(con_names)}" - ) - for cons in scip_cons: - if cons.getConshdlrName() in ["linear", "nonlinear"]: - soln_constraints[cons.name] = {} - pyomo_con = self._solver_con_to_pyomo_con_map[cons.name] - scip_expr = self._pyomo_con_to_solver_expr_map[pyomo_con] - activity = scip_sol[scip_expr] - if pyomo_con.has_lb(): - lhs = value(pyomo_con.lower) - else: - lhs = -1e20 - if pyomo_con.has_ub(): - rhs = value(pyomo_con.upper) - else: - rhs = 1e20 - soln_constraints[cons.name]["Slack"] = min( - activity - lhs, rhs - activity - ) - elif self._load_solutions: if scip.getNSols() > 0: self.load_vars() - if extract_slacks: - self._load_slacks() - self.results.solution.insert(soln) # finally, clean any temporary files registered with the temp file @@ -768,26 +731,7 @@ def _load_duals(self, cons_to_load=None): ) def _load_slacks(self, cons_to_load=None): - if not hasattr(self._pyomo_model, "slack"): - self._pyomo_model.slack = Suffix(direction=Suffix.IMPORT) - slack = self._pyomo_model.slack - scip_sol = self._solver_model.getBestSol() - - if cons_to_load is None: - scip_cons = list(self._pyomo_con_to_solver_con_expr_map.values()) - else: - scip_cons = [ - self._pyomo_con_to_solver_con_expr_map[pyomo_cons] - for pyomo_cons in cons_to_load - ] - for cons in scip_cons: - if cons.getConshdlrName() in ["linear", "nonlinear"]: - pyomo_con = self._solver_con_to_pyomo_con_map[cons.name] - scip_expr = self._pyomo_con_to_solver_expr_map[pyomo_con] - activity = scip_sol[scip_expr] - rhs = self._solver_model.getRhs(cons) - lhs = self._solver_model.getLhs(cons) - slack[pyomo_con] = min(activity - lhs, rhs - activity) + raise NotImplementedError("SCIP via Pyomo does not support slack loading") def load_duals(self, cons_to_load=None): """ diff --git a/pyomo/solvers/plugins/solvers/scip_persistent.py b/pyomo/solvers/plugins/solvers/scip_persistent.py index 880380ced1f..e3fe9e37b5d 100644 --- a/pyomo/solvers/plugins/solvers/scip_persistent.py +++ b/pyomo/solvers/plugins/solvers/scip_persistent.py @@ -49,34 +49,19 @@ def _remove_constraint(self, solver_conname): con = self._solver_con_to_pyomo_con_map[solver_conname] scip_con = self._pyomo_con_to_solver_con_expr_map[con] self._solver_model.delCons(scip_con) - for var in self._vars_reference_by_con[con]: - self._references_vars[var] -= 1 - del self._vars_reference_by_con[con] - del self._pyomo_con_to_solver_con_map[con] del self._pyomo_con_to_solver_con_expr_map[con] - del self._pyomo_con_to_solver_expr_map[con] - del self._solver_con_to_pyomo_con_map[solver_conname] - def _remove_sos_constraint(self, solver_sos_conname): con = self._solver_con_to_pyomo_con_map[solver_sos_conname] scip_con = self._pyomo_con_to_solver_con_expr_map[con] self._solver_model.delCons(scip_con) - for var in self._vars_reference_by_con[con]: - self._references_vars[var] -= 1 - del self._vars_reference_by_con[con] - del self._pyomo_con_to_solver_con_map[con] del self._pyomo_con_to_solver_con_expr_map[con] - del self._solver_con_to_pyomo_con_map[solver_conname] def _remove_var(self, solver_varname): var = self._solver_var_to_pyomo_var_map[solver_varname] scip_var = self._pyomo_var_to_solver_var_expr_map[var] self._solver_model.delVar(scip_var) del self._pyomo_var_to_solver_var_expr_map[var] - del self._pyomo_var_to_solver_var_map[var] - del self._solver_var_to_pyomo_var_map[scip_var.name] - del self._referenced_variables[var] def _warm_start(self): SCIPDirect._warm_start(self) From f703d1f71128a95d509aa9ea0b08d12de2dcb41a Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Mon, 29 Apr 2024 10:38:10 +0200 Subject: [PATCH 19/25] Remove dual loading test for SCIP --- pyomo/solvers/tests/checks/test_SCIPDirect.py | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/pyomo/solvers/tests/checks/test_SCIPDirect.py b/pyomo/solvers/tests/checks/test_SCIPDirect.py index cc9e114fed1..5863a54bdcb 100644 --- a/pyomo/solvers/tests/checks/test_SCIPDirect.py +++ b/pyomo/solvers/tests/checks/test_SCIPDirect.py @@ -86,25 +86,6 @@ def test_optimal_lp(self): self.assertEqual(results.solution.status, SolutionStatus.optimal) - def test_get_duals_lp(self): - with SolverFactory("scip_direct", solver_io="python") as opt: - model = ConcreteModel() - model.X = Var(within=NonNegativeReals) - model.Y = Var(within=NonNegativeReals) - - model.C1 = Constraint(expr=2 * model.X + model.Y >= 8) - model.C2 = Constraint(expr=model.X + 3 * model.Y >= 6) - - model.O = Objective(expr=model.X + model.Y) - - results = opt.solve(model, suffixes=["dual"], load_solutions=False) - - model.dual = Suffix(direction=Suffix.IMPORT) - model.solutions.load_from(results) - - self.assertAlmostEqual(model.dual[model.C1], 0.4) - self.assertAlmostEqual(model.dual[model.C2], 0.2) - def test_infeasible_mip(self): with SolverFactory("scip_direct", solver_io="python") as opt: model = ConcreteModel() From 5c02d32009990b8054440f0a6049bdf934247a79 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Mon, 29 Apr 2024 10:42:00 +0200 Subject: [PATCH 20/25] Remove slack for suffix in tests --- pyomo/solvers/plugins/solvers/scip_direct.py | 2 -- pyomo/solvers/tests/solvers.py | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/pyomo/solvers/plugins/solvers/scip_direct.py b/pyomo/solvers/plugins/solvers/scip_direct.py index 57cfc213f3d..a965e66362e 100644 --- a/pyomo/solvers/plugins/solvers/scip_direct.py +++ b/pyomo/solvers/plugins/solvers/scip_direct.py @@ -10,7 +10,6 @@ # ___________________________________________________________________________ import logging -import re import sys from pyomo.common.collections import ComponentSet, ComponentMap, Bunch @@ -36,7 +35,6 @@ from pyomo.opt.results.solution import Solution, SolutionStatus from pyomo.opt.results.solver import TerminationCondition, SolverStatus from pyomo.opt.base import SolverFactory -from pyomo.core.base.suffix import Suffix logger = logging.getLogger("pyomo.solvers") diff --git a/pyomo/solvers/tests/solvers.py b/pyomo/solvers/tests/solvers.py index b66c1ca5af5..ba1530c67cc 100644 --- a/pyomo/solvers/tests/solvers.py +++ b/pyomo/solvers/tests/solvers.py @@ -388,7 +388,7 @@ def test_solver_cases(*args): name="scip_persistent", io="python", capabilities=_scip_persistent_capabilities, - import_suffixes=["slack"], + import_suffixes=[], ) # From 8ebcf88365267e28a5b820eedce12a0d1bf5473c Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Mon, 29 Apr 2024 11:13:28 +0200 Subject: [PATCH 21/25] Remove TODO for nonlinear handling --- pyomo/solvers/plugins/solvers/scip_direct.py | 1 - 1 file changed, 1 deletion(-) diff --git a/pyomo/solvers/plugins/solvers/scip_direct.py b/pyomo/solvers/plugins/solvers/scip_direct.py index a965e66362e..9061deac6ad 100644 --- a/pyomo/solvers/plugins/solvers/scip_direct.py +++ b/pyomo/solvers/plugins/solvers/scip_direct.py @@ -161,7 +161,6 @@ def _get_expr_from_pyomo_repn(self, repn, max_degree=None): referenced_vars.add(x) referenced_vars.add(y) - # TODO: Introduce handling on non-linear expressions if repn.nonlinear_expr is not None: def get_nl_expr_recursively(pyomo_expr): From 30d8cc62d5903e9e8dce4f0cabe79f22a5aba495 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Thu, 13 Jun 2024 14:48:43 +0200 Subject: [PATCH 22/25] Skip LP_trivial_constraints for SCIP persistent --- pyomo/solvers/plugins/solvers/scip_direct.py | 6 ++++-- pyomo/solvers/tests/testcases.py | 9 +++++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/pyomo/solvers/plugins/solvers/scip_direct.py b/pyomo/solvers/plugins/solvers/scip_direct.py index 9061deac6ad..39c3a4fd996 100644 --- a/pyomo/solvers/plugins/solvers/scip_direct.py +++ b/pyomo/solvers/plugins/solvers/scip_direct.py @@ -67,8 +67,10 @@ def _init(self): self._scip = pyscipopt self._python_api_exists = True - self._version = str(self._scip.Model().version()) - self._version_major = self._version.split(".")[0] + self._version = tuple( + int(k) for k in str(self._scip.Model().version()).split(".") + ) + self._version_major = self._version[0] except ImportError: self._python_api_exists = False except Exception as e: diff --git a/pyomo/solvers/tests/testcases.py b/pyomo/solvers/tests/testcases.py index 6bef40818d9..f586e22b1e1 100644 --- a/pyomo/solvers/tests/testcases.py +++ b/pyomo/solvers/tests/testcases.py @@ -248,6 +248,15 @@ "inside NL files. A ticket has been filed.", ) +# +# SCIP Persistent +# + +ExpectedFailures["scip_persistent", "python", "LP_trivial_constraints"] = ( + lambda v: v <= _trunk_version, + "SCIP does not allow empty constraints with no variables to be added to the Model.", +) + # # BARON # From 30e5e65bfd063b049f950b9f74c0d238187077ca Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Thu, 13 Jun 2024 16:12:24 +0200 Subject: [PATCH 23/25] Add transformation for add_cons with non float/int rhs e.g. np.int --- pyomo/solvers/plugins/solvers/scip_direct.py | 21 ++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/pyomo/solvers/plugins/solvers/scip_direct.py b/pyomo/solvers/plugins/solvers/scip_direct.py index 39c3a4fd996..7c26670c2b4 100644 --- a/pyomo/solvers/plugins/solvers/scip_direct.py +++ b/pyomo/solvers/plugins/solvers/scip_direct.py @@ -318,29 +318,38 @@ def _add_constraint(self, con): if con.has_lb(): if not is_fixed(con.lower): raise ValueError(f"Lower bound of constraint {con} is not constant.") + con_lower = value(con.lower) + if not isinstance(con_lower, (float, int)): + con_lower = float(con_lower) if con.has_ub(): if not is_fixed(con.upper): raise ValueError(f"Upper bound of constraint {con} is not constant.") + con_upper = value(con.upper) + if not isinstance(con_upper, (float, int)): + con_upper = float(con_upper) if con.equality: scip_cons = self._solver_model.addCons( - scip_expr == value(con.lower), name=conname + scip_expr == con_lower, name=conname ) elif con.has_lb() and con.has_ub(): scip_cons = self._solver_model.addCons( - value(con.lower) <= scip_expr, name=conname + con_lower <= scip_expr, name=conname ) - rhs = value(con.upper) + rhs = con_upper if hasattr(con.body, "constant"): - rhs -= value(con.body.constant) + con_constant = value(con.body.constant) + if not isinstance(con_constant, (float, int)): + con_body = float(con_constant) + rhs -= con_constant self._solver_model.chgRhs(scip_cons, rhs) elif con.has_lb(): scip_cons = self._solver_model.addCons( - value(con.lower) <= scip_expr, name=conname + con_lower <= scip_expr, name=conname ) elif con.has_ub(): scip_cons = self._solver_model.addCons( - scip_expr <= value(con.upper), name=conname + scip_expr <= con_upper, name=conname ) else: raise ValueError( From 9104a921c55f9cd170bd1a7e93e1628869de2360 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Thu, 13 Jun 2024 16:35:41 +0200 Subject: [PATCH 24/25] Add warning if type is converted. Tidy up logic --- pyomo/solvers/plugins/solvers/scip_direct.py | 30 ++++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/pyomo/solvers/plugins/solvers/scip_direct.py b/pyomo/solvers/plugins/solvers/scip_direct.py index 7c26670c2b4..6ce98d80e27 100644 --- a/pyomo/solvers/plugins/solvers/scip_direct.py +++ b/pyomo/solvers/plugins/solvers/scip_direct.py @@ -319,38 +319,38 @@ def _add_constraint(self, con): if not is_fixed(con.lower): raise ValueError(f"Lower bound of constraint {con} is not constant.") con_lower = value(con.lower) - if not isinstance(con_lower, (float, int)): + if type(con_lower) != float and type(con_lower) != int: + logger.warning( + f"Constraint {conname} has LHS type {type(value(con.lower))}. " + f"Converting to float as type is not allowed for SCIP." + ) con_lower = float(con_lower) if con.has_ub(): if not is_fixed(con.upper): raise ValueError(f"Upper bound of constraint {con} is not constant.") con_upper = value(con.upper) - if not isinstance(con_upper, (float, int)): + if type(con_upper) != float and type(con_upper) != int: + logger.warning( + f"Constraint {conname} has RHS type {type(value(con.upper))}. " + f"Converting to float as type is not allowed for SCIP." + ) con_upper = float(con_upper) if con.equality: - scip_cons = self._solver_model.addCons( - scip_expr == con_lower, name=conname - ) + scip_cons = self._solver_model.addCons(scip_expr == con_lower, name=conname) elif con.has_lb() and con.has_ub(): - scip_cons = self._solver_model.addCons( - con_lower <= scip_expr, name=conname - ) + scip_cons = self._solver_model.addCons(con_lower <= scip_expr, name=conname) rhs = con_upper if hasattr(con.body, "constant"): con_constant = value(con.body.constant) if not isinstance(con_constant, (float, int)): - con_body = float(con_constant) + con_constant = float(con_constant) rhs -= con_constant self._solver_model.chgRhs(scip_cons, rhs) elif con.has_lb(): - scip_cons = self._solver_model.addCons( - con_lower <= scip_expr, name=conname - ) + scip_cons = self._solver_model.addCons(con_lower <= scip_expr, name=conname) elif con.has_ub(): - scip_cons = self._solver_model.addCons( - scip_expr <= con_upper, name=conname - ) + scip_cons = self._solver_model.addCons(scip_expr <= con_upper, name=conname) else: raise ValueError( f"Constraint does not have a lower or an upper bound: {con} \n" From f3f2d7c0334afbea37e9d8d24d60227f333ee4c1 Mon Sep 17 00:00:00 2001 From: Mark Turner Date: Wed, 20 Nov 2024 17:13:03 +0100 Subject: [PATCH 25/25] Fix num. vars and cons from transformed. Silent warm start fail --- pyomo/solvers/plugins/solvers/scip_direct.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/pyomo/solvers/plugins/solvers/scip_direct.py b/pyomo/solvers/plugins/solvers/scip_direct.py index 6ce98d80e27..89dd25b86ee 100644 --- a/pyomo/solvers/plugins/solvers/scip_direct.py +++ b/pyomo/solvers/plugins/solvers/scip_direct.py @@ -641,10 +641,9 @@ def _postsolve(self): except TypeError: soln.gap = None - # TODO: Should these values be of the transformed or the original problem? - self.results.problem.number_of_constraints = scip.getNConss() + self.results.problem.number_of_constraints = scip.getNConss(transformed=False) # self.results.problem.number_of_nonzeros = None - self.results.problem.number_of_variables = scip.getNVars() + self.results.problem.number_of_variables = scip.getNVars(transformed=False) self.results.problem.number_of_binary_variables = n_bin_vars self.results.problem.number_of_integer_variables = n_int_vars self.results.problem.number_of_continuous_variables = n_con_vars @@ -704,16 +703,13 @@ def _warm_start(self): scip_sol[scip_var] = value(pyomo_var) if partial_sol: self._solver_model.addSol(scip_sol) - del scip_sol else: - feasible = self._solver_model.checkSol(scip_sol) + feasible = self._solver_model.checkSol(scip_sol, printreason=not self._tee) if feasible: self._solver_model.addSol(scip_sol) - del scip_sol else: logger.warning("Warm start solution was not accepted by SCIP") self._solver_model.freeSol(scip_sol) - del scip_sol def _load_vars(self, vars_to_load=None): var_map = self._pyomo_var_to_solver_var_expr_map