def test_items(self): cmap = ComponentMap(self._components) for x in cmap.items(): self.assertEqual(type(x), tuple) self.assertEqual(len(x), 2) self.assertEqual( sorted(cmap.items(), key=lambda _x: (id(_x[0]), _x[1])), sorted(self._components, key=lambda _x: (id(_x[0]), _x[1])))
def test_items(self): cmap = ComponentMap(self._components) for x in cmap.items(): self.assertEqual(type(x), tuple) self.assertEqual(len(x), 2) self.assertEqual(sorted(cmap.items(), key=lambda _x: (id(_x[0]), _x[1])), sorted(self._components, key=lambda _x: (id(_x[0]), _x[1])))
def _collect_expression_types(quadratic, linear): """Collect different expression types from quadratic and linear expression. Given an expression a0 x0^2 + a1 x1^2 + ...+ b0 x0 + b1 x1 + b2 x2 + ... Returns the quadratic univariate expressions like `a0 x0^2`, the bilinear expressions `c x1 x2`, and the linear expressions `bn xn`. :param quadratic: the quadratic expression :param linear: the linear expression :return: A tuple (univariate_expr, bilinear_terms, linear_terms) """ variables_coef = ComponentMap() for coef, var in zip(linear.linear_coefs, linear.linear_vars): variables_coef[var] = \ _VariableCoefficients(quadratic=0.0, linear=coef) non_univariate_terms = [] for term in quadratic.terms: if term.var1 is term.var2: var_coef = variables_coef.get(term.var1, None) if var_coef is None: non_univariate_terms.append(term) else: linear_coef = var_coef.linear var_coef = _VariableCoefficients(quadratic=term.coefficient, linear=linear_coef) variables_coef[term.var1] = var_coef else: non_univariate_terms.append(term) linear_terms = [(v, vc.linear) for v, vc in variables_coef.items() if almosteq(vc.quadratic, 0.0)] univariate_terms = [(v, vc.quadratic, vc.linear) for v, vc in variables_coef.items() if not almosteq(vc.quadratic, 0.0)] return univariate_terms, non_univariate_terms, linear_terms
class GurobiDirect(DirectSolver): alias('gurobi_direct', doc='Direct python interface to Gurobi') def __init__(self, **kwds): kwds['type'] = 'gurobi_direct' DirectSolver.__init__(self, **kwds) self._pyomo_var_to_solver_var_map = ComponentMap() self._solver_var_to_pyomo_var_map = ComponentMap() self._pyomo_con_to_solver_con_map = dict() self._solver_con_to_pyomo_con_map = ComponentMap() self._init() def _init(self): self._name = None try: import gurobipy self._gurobipy = gurobipy self._python_api_exists = True self._version = self._gurobipy.gurobi.version() self._name = "Gurobi %s.%s%s" % self._version while len(self._version) < 4: self._version += (0, ) self._version = self._version[:4] self._version_major = self._version[0] except ImportError: self._python_api_exists = False except Exception as e: # other forms of exceptions can be thrown by the gurobi python # import. for example, a gurobipy.GurobiError exception is thrown # if all tokens for Gurobi are already in use. assuming, of # course, the license is a token license. unfortunately, you can't # import without a license, which means we can't test for the # exception above! print("Import of gurobipy failed - gurobi message=" + str(e) + "\n") self._python_api_exists = False self._range_constraints = set() self._max_obj_degree = 2 self._max_constraint_degree = 2 # Note: Undefined capabilites default to None self._capabilities.linear = True self._capabilities.quadratic_objective = True self._capabilities.quadratic_constraint = True self._capabilities.integer = True self._capabilities.sos1 = True self._capabilities.sos2 = True # fix for compatibility with pre-5.0 Gurobi if self._python_api_exists and \ (self._version_major < 5): self._max_constraint_degree = 1 self._capabilities.quadratic_constraint = False def _apply_solver(self): if not self._save_results: for block in self._pyomo_model.block_data_objects( descend_into=True, active=True): for var in block.component_data_objects( ctype=pyomo.core.base.var.Var, descend_into=False, active=True, sort=False): var.stale = True if self._tee: self._solver_model.setParam('OutputFlag', 1) else: self._solver_model.setParam('OutputFlag', 0) self._solver_model.setParam('LogFile', self._log_file) if self._keepfiles: print("Solver log file: " + self._log_file) # Options accepted by gurobi (case insensitive): # ['Cutoff', 'IterationLimit', 'NodeLimit', 'SolutionLimit', 'TimeLimit', # 'FeasibilityTol', 'IntFeasTol', 'MarkowitzTol', 'MIPGap', 'MIPGapAbs', # 'OptimalityTol', 'PSDTol', 'Method', 'PerturbValue', 'ObjScale', 'ScaleFlag', # 'SimplexPricing', 'Quad', 'NormAdjust', 'BarIterLimit', 'BarConvTol', # 'BarCorrectors', 'BarOrder', 'Crossover', 'CrossoverBasis', 'BranchDir', # 'Heuristics', 'MinRelNodes', 'MIPFocus', 'NodefileStart', 'NodefileDir', # 'NodeMethod', 'PumpPasses', 'RINS', 'SolutionNumber', 'SubMIPNodes', 'Symmetry', # 'VarBranch', 'Cuts', 'CutPasses', 'CliqueCuts', 'CoverCuts', 'CutAggPasses', # 'FlowCoverCuts', 'FlowPathCuts', 'GomoryPasses', 'GUBCoverCuts', 'ImpliedCuts', # 'MIPSepCuts', 'MIRCuts', 'NetworkCuts', 'SubMIPCuts', 'ZeroHalfCuts', 'ModKCuts', # 'Aggregate', 'AggFill', 'PreDual', 'DisplayInterval', 'IISMethod', 'InfUnbdInfo', # 'LogFile', 'PreCrush', 'PreDepRow', 'PreMIQPMethod', 'PrePasses', 'Presolve', # 'ResultFile', 'ImproveStartTime', 'ImproveStartGap', 'Threads', 'Dummy', 'OutputFlag'] for key, option in self.options.items(): # When options come from the pyomo command, all # values are string types, so we try to cast # them to a numeric value in the event that # setting the parameter fails. try: self._solver_model.setParam(key, option) except TypeError: # we place the exception handling for # checking the cast of option to a float in # another function so that we can simply # call raise here instead of except # TypeError as e / raise e, because the # latter does not preserve the Gurobi stack # trace if not _is_numeric(option): raise self._solver_model.setParam(key, float(option)) if self._version_major >= 5: for suffix in self._suffixes: if re.match(suffix, "dual"): self._solver_model.setParam( self._gurobipy.GRB.Param.QCPDual, 1) self._solver_model.optimize() self._solver_model.setParam('LogFile', 'default') # FIXME: can we get a return code indicating if Gurobi had a significant failure? return Bunch(rc=None, log=None) def _get_expr_from_pyomo_repn(self, repn, max_degree=2): referenced_vars = ComponentSet() degree = canonical_degree(repn) if (degree is None) or (degree > max_degree): raise DegreeError( 'GurobiDirect does not support expressions of degree {0}.'. format(degree)) if isinstance(repn, LinearCanonicalRepn): if (repn.linear is not None) and (len(repn.linear) > 0): list(map(referenced_vars.add, repn.variables)) new_expr = self._gurobipy.LinExpr(repn.linear, [ self._pyomo_var_to_solver_var_map[i] for i in repn.variables ]) else: new_expr = 0 if repn.constant is not None: new_expr += repn.constant else: new_expr = 0 if 0 in repn: new_expr += repn[0][None] if 1 in repn: for ndx, coeff in repn[1].items(): new_expr += coeff * self._pyomo_var_to_solver_var_map[ repn[-1][ndx]] referenced_vars.add(repn[-1][ndx]) if 2 in repn: for key, coeff in repn[2].items(): tmp_expr = coeff for ndx, power in key.items(): referenced_vars.add(repn[-1][ndx]) for i in range(power): tmp_expr *= self._pyomo_var_to_solver_var_map[ repn[-1][ndx]] new_expr += tmp_expr return new_expr, referenced_vars def _get_expr_from_pyomo_expr(self, expr, max_degree=2): repn = generate_canonical_repn(expr) try: gurobi_expr, referenced_vars = self._get_expr_from_pyomo_repn( repn, max_degree) except DegreeError as e: msg = e.args[0] msg += '\nexpr: {0}'.format(expr) raise DegreeError(msg) return gurobi_expr, referenced_vars def _add_var(self, var): varname = self._symbol_map.getSymbol(var, self._labeler) vtype = self._gurobi_vtype_from_var(var) if var.has_lb(): lb = value(var.lb) else: lb = -self._gurobipy.GRB.INFINITY if var.has_ub(): ub = value(var.ub) else: ub = self._gurobipy.GRB.INFINITY gurobipy_var = self._solver_model.addVar(lb=lb, ub=ub, vtype=vtype, name=varname) self._pyomo_var_to_solver_var_map[var] = gurobipy_var self._solver_var_to_pyomo_var_map[gurobipy_var] = var self._referenced_variables[var] = 0 if var.is_fixed(): gurobipy_var.setAttr('lb', var.value) gurobipy_var.setAttr('ub', var.value) def _set_instance(self, model, kwds={}): self._range_constraints = set() DirectOrPersistentSolver._set_instance(self, model, kwds) self._pyomo_con_to_solver_con_map = dict() self._solver_con_to_pyomo_con_map = ComponentMap() self._pyomo_var_to_solver_var_map = ComponentMap() self._solver_var_to_pyomo_var_map = ComponentMap() try: if model.name is not None: self._solver_model = self._gurobipy.Model(model.name) else: self._solver_model = self._gurobipy.Model() except Exception: e = sys.exc_info()[1] msg = ( 'Unable to create Gurobi model. Have you installed the Python bindings for Gurboi?\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 Gurobi instance." % ( var.name, self._pyomo_model.name, )) def _add_block(self, block): DirectOrPersistentSolver._add_block(self, block) self._solver_model.update() def _add_constraint(self, con): if not con.active: return None if is_fixed(con.body): if self._skip_trivial_constraints: return None conname = self._symbol_map.getSymbol(con, self._labeler) if con._linear_canonical_form: gurobi_expr, referenced_vars = self._get_expr_from_pyomo_repn( con.canonical_form(), self._max_constraint_degree) elif isinstance(con, LinearCanonicalRepn): gurobi_expr, referenced_vars = self._get_expr_from_pyomo_repn( con, self._max_constraint_degree) else: gurobi_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: gurobipy_con = self._solver_model.addConstr( lhs=gurobi_expr, sense=self._gurobipy.GRB.EQUAL, rhs=value(con.lower), name=conname) elif con.has_lb() and (value(con.lower) > -float('inf')) and con.has_ub() and (value( con.upper) < float('inf')): gurobipy_con = self._solver_model.addRange(gurobi_expr, value(con.lower), value(con.upper), name=conname) self._range_constraints.add(con) elif con.has_lb() and (value(con.lower) > -float('inf')): gurobipy_con = self._solver_model.addConstr( lhs=gurobi_expr, sense=self._gurobipy.GRB.GREATER_EQUAL, rhs=value(con.lower), name=conname) elif con.has_ub() and (value(con.upper) < float('inf')): gurobipy_con = self._solver_model.addConstr( lhs=gurobi_expr, sense=self._gurobipy.GRB.LESS_EQUAL, rhs=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_map[con] = gurobipy_con self._solver_con_to_pyomo_con_map[gurobipy_con] = 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 == 1: sos_type = self._gurobipy.GRB.SOS_TYPE1 elif level == 2: sos_type = self._gurobipy.GRB.SOS_TYPE2 else: raise ValueError( 'Solver does not support SOS level {0} constraints'.format( level)) gurobi_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) gurobi_vars.append(self._pyomo_var_to_solver_var_map[v]) self._referenced_variables[v] += 1 weights.append(w) gurobipy_con = self._solver_model.addSOS(sos_type, gurobi_vars, weights) self._pyomo_con_to_solver_con_map[con] = gurobipy_con self._solver_con_to_pyomo_con_map[gurobipy_con] = con def _gurobi_vtype_from_var(self, var): """ This function takes a pyomo variable and returns the appropriate gurobi variable type :param var: pyomo.core.base.var.Var :return: gurobipy.GRB.CONTINUOUS or gurobipy.GRB.BINARY or gurobipy.GRB.INTEGER """ if var.is_binary(): vtype = self._gurobipy.GRB.BINARY elif var.is_integer(): vtype = self._gurobipy.GRB.INTEGER elif var.is_continuous(): vtype = self._gurobipy.GRB.CONTINUOUS 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 == pyomo.core.kernel.minimize: sense = self._gurobipy.GRB.MINIMIZE elif obj.sense == pyomo.core.kernel.maximize: sense = self._gurobipy.GRB.MAXIMIZE else: raise ValueError('Objective sense is not recognized: {0}'.format( obj.sense)) gurobi_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(gurobi_expr, sense=sense) self._objective = obj self._vars_referenced_by_obj = referenced_vars def _postsolve(self): # the only suffixes that we extract from GUROBI 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 gurobi_direct solver plugin cannot extract solution suffix=" + suffix) gprob = self._solver_model grb = self._gurobipy.GRB status = gprob.Status if gprob.getAttr(self._gurobipy.GRB.Attr.IsMIP): 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 = self._name self.results.solver.wallclock_time = gprob.Runtime if status == grb.LOADED: # problem is loaded, but no solution 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 == grb.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 == grb.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 == grb.INF_OR_UNBD: 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 == grb.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 == grb.CUTOFF: self.results.solver.status = SolverStatus.aborted self.results.solver.termination_message = "Optimal objective for model was proven to be worse than the " \ "value specified in the Cutoff parameter. No solution " \ "information is available." self.results.solver.termination_condition = TerminationCondition.minFunctionValue soln.status = SolutionStatus.unknown elif status == grb.ITERATION_LIMIT: self.results.solver.status = SolverStatus.aborted self.results.solver.termination_message = "Optimization terminated because the total number of simplex " \ "iterations performed exceeded the value specified in the " \ "IterationLimit parameter." self.results.solver.termination_condition = TerminationCondition.maxIterations soln.status = SolutionStatus.stoppedByLimit elif status == grb.NODE_LIMIT: self.results.solver.status = SolverStatus.aborted self.results.solver.termination_message = "Optimization terminated because the total number of " \ "branch-and-cut nodes explored exceeded the value specified " \ "in the NodeLimit parameter" self.results.solver.termination_condition = TerminationCondition.maxEvaluations soln.status = SolutionStatus.stoppedByLimit elif status == grb.TIME_LIMIT: self.results.solver.status = SolverStatus.aborted self.results.solver.termination_message = "Optimization terminated because the time expended exceeded " \ "the value specified in the TimeLimit parameter." self.results.solver.termination_condition = TerminationCondition.maxTimeLimit soln.status = SolutionStatus.stoppedByLimit elif status == grb.SOLUTION_LIMIT: 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 SolutionLimit parameter." self.results.solver.termination_condition = TerminationCondition.unknown soln.status = SolutionStatus.stoppedByLimit elif status == grb.INTERRUPTED: 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 elif status == grb.NUMERIC: self.results.solver.status = SolverStatus.error self.results.solver.termination_message = "Optimization was terminated due to unrecoverable numerical " \ "difficulties." self.results.solver.termination_condition = TerminationCondition.error soln.status = SolutionStatus.error elif status == grb.SUBOPTIMAL: self.results.solver.status = SolverStatus.warning self.results.solver.termination_message = "Unable to satisfy optimality tolerances; a sub-optimal " \ "solution is available." self.results.solver.termination_condition = TerminationCondition.other soln.status = SolutionStatus.feasible else: self.results.solver.status = SolverStatus.error self.results.solver.termination_message = "Unknown return code from GUROBI." self.results.solver.termination_condition = TerminationCondition.error soln.status = SolutionStatus.error self.results.problem.name = gprob.ModelName if gprob.ModelSense == 1: self.results.problem.sense = pyomo.core.kernel.minimize elif gprob.ModelSense == -1: self.results.problem.sense = pyomo.core.kernel.maximize else: raise RuntimeError( 'Unrecognized gurobi objective sense: {0}'.format( gprob.ModelSense)) self.results.problem.upper_bound = None self.results.problem.lower_bound = None if (gprob.NumBinVars + gprob.NumIntVars) == 0: try: self.results.problem.upper_bound = gprob.ObjVal self.results.problem.lower_bound = gprob.ObjVal except (self._gurobipy.GurobiError, AttributeError): pass elif gprob.ModelSense == 1: # minimizing try: self.results.problem.upper_bound = gprob.ObjVal except (self._gurobipy.GurobiError, AttributeError): pass try: self.results.problem.lower_bound = gprob.ObjBound except (self._gurobipy.GurobiError, AttributeError): pass elif gprob.ModelSense == -1: # maximizing try: self.results.problem.upper_bound = gprob.ObjBound except (self._gurobipy.GurobiError, AttributeError): pass try: self.results.problem.lower_bound = gprob.ObjVal except (self._gurobipy.GurobiError, AttributeError): pass else: raise RuntimeError( 'Unrecognized gurobi objective sense: {0}'.format( gprob.ModelSense)) try: soln.gap = self.results.problem.upper_bound - self.results.problem.lower_bound except TypeError: soln.gap = None self.results.problem.number_of_constraints = gprob.NumConstrs + gprob.NumQConstrs + gprob.NumSOS self.results.problem.number_of_nonzeros = gprob.NumNZs self.results.problem.number_of_variables = gprob.NumVars self.results.problem.number_of_binary_variables = gprob.NumBinVars self.results.problem.number_of_integer_variables = gprob.NumIntVars self.results.problem.number_of_continuous_variables = gprob.NumVars - gprob.NumIntVars - gprob.NumBinVars self.results.problem.number_of_objectives = 1 self.results.problem.number_of_solutions = gprob.SolCount # 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 compatability. It is more efficient to set _save_results to False and use load_vars, load_duals, etc. """ if gprob.SolCount > 0: soln_variables = soln.variable soln_constraints = soln.constraint gurobi_vars = self._solver_model.getVars() gurobi_vars = list( set(gurobi_vars).intersection( set(self._pyomo_var_to_solver_var_map.values()))) var_vals = self._solver_model.getAttr("X", gurobi_vars) names = self._solver_model.getAttr("VarName", gurobi_vars) for gurobi_var, val, name in zip(gurobi_vars, var_vals, names): pyomo_var = self._solver_var_to_pyomo_var_map[gurobi_var] if self._referenced_variables[pyomo_var] > 0: pyomo_var.stale = False soln_variables[name] = {"Value": val} if extract_reduced_costs: vals = self._solver_model.getAttr("Rc", gurobi_vars) for gurobi_var, val, name in zip(gurobi_vars, vals, names): pyomo_var = self._solver_var_to_pyomo_var_map[ gurobi_var] if self._referenced_variables[pyomo_var] > 0: soln_variables[name]["Rc"] = val if extract_duals or extract_slacks: gurobi_cons = self._solver_model.getConstrs() con_names = self._solver_model.getAttr( "ConstrName", gurobi_cons) for name in con_names: soln_constraints[name] = {} if self._version_major >= 5: gurobi_q_cons = self._solver_model.getQConstrs() q_con_names = self._solver_model.getAttr( "QCName", gurobi_q_cons) for name in q_con_names: soln_constraints[name] = {} if extract_duals: vals = self._solver_model.getAttr("Pi", gurobi_cons) for val, name in zip(vals, con_names): soln_constraints[name]["Dual"] = val if self._version_major >= 5: q_vals = self._solver_model.getAttr( "QCPi", gurobi_q_cons) for val, name in zip(q_vals, q_con_names): soln_constraints[name]["Dual"] = val if extract_slacks: gurobi_range_con_vars = set( self._solver_model.getVars()) - set( self._pyomo_var_to_solver_var_map.values()) vals = self._solver_model.getAttr("Slack", gurobi_cons) for gurobi_con, val, name in zip(gurobi_cons, vals, con_names): pyomo_con = self._solver_con_to_pyomo_con_map[ gurobi_con] if pyomo_con in self._range_constraints: lin_expr = self._solver_model.getRow(gurobi_con) for i in reversed(range(lin_expr.size())): v = lin_expr.getVar(i) if v in gurobi_range_con_vars: Us_ = v.X Ls_ = v.UB - v.X if Us_ > Ls_: soln_constraints[name]["Slack"] = Us_ else: soln_constraints[name]["Slack"] = -Ls_ break else: soln_constraints[name]["Slack"] = val if self._version_major >= 5: q_vals = self._solver_model.getAttr( "QCSlack", gurobi_q_cons) for val, name in zip(q_vals, q_con_names): soln_constraints[name]["Slack"] = val elif self._load_solutions: if gprob.SolCount > 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. pyutilib.services.TempfileManager.pop(remove=not self._keepfiles) return DirectOrPersistentSolver._postsolve(self) def warm_start_capable(self): return True def _warm_start(self): for pyomo_var, gurobipy_var in self._pyomo_var_to_solver_var_map.items( ): if pyomo_var.value is not None: gurobipy_var.setAttr(self._gurobipy.GRB.Attr.Start, value(pyomo_var)) def _load_vars(self, vars_to_load=None): var_map = self._pyomo_var_to_solver_var_map ref_vars = self._referenced_variables if vars_to_load is None: vars_to_load = var_map.keys() gurobi_vars_to_load = [ var_map[pyomo_var] for pyomo_var in vars_to_load ] vals = self._solver_model.getAttr("X", gurobi_vars_to_load) for var, val in zip(vars_to_load, vals): if ref_vars[var] > 0: var.stale = False var.value = val 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_map ref_vars = self._referenced_variables rc = self._pyomo_model.rc if vars_to_load is None: vars_to_load = var_map.keys() gurobi_vars_to_load = [ var_map[pyomo_var] for pyomo_var in vars_to_load ] vals = self._solver_model.getAttr("Rc", gurobi_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 if cons_to_load is None: linear_cons_to_load = self._solver_model.getConstrs() if self._version_major >= 5: quadratic_cons_to_load = self._solver_model.getQConstrs() else: gurobi_cons_to_load = set( [con_map[pyomo_con] for pyomo_con in cons_to_load]) linear_cons_to_load = gurobi_cons_to_load.intersection( set(self._solver_model.getConstrs())) if self._version_major >= 5: quadratic_cons_to_load = gurobi_cons_to_load.intersection( set(self._solver_model.getQConstrs())) linear_vals = self._solver_model.getAttr("Pi", linear_cons_to_load) if self._version_major >= 5: quadratic_vals = self._solver_model.getAttr( "QCPi", quadratic_cons_to_load) for gurobi_con, val in zip(linear_cons_to_load, linear_vals): pyomo_con = reverse_con_map[gurobi_con] dual[pyomo_con] = val if self._version_major >= 5: for gurobi_con, val in zip(quadratic_cons_to_load, quadratic_vals): pyomo_con = reverse_con_map[gurobi_con] dual[pyomo_con] = val 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 gurobi_range_con_vars = set(self._solver_model.getVars()) - set( self._pyomo_var_to_solver_var_map.values()) if cons_to_load is None: linear_cons_to_load = self._solver_model.getConstrs() if self._version_major >= 5: quadratic_cons_to_load = self._solver_model.getQConstrs() else: gurobi_cons_to_load = set( [con_map[pyomo_con] for pyomo_con in cons_to_load]) linear_cons_to_load = gurobi_cons_to_load.intersection( set(self._solver_model.getConstrs())) if self._version_major >= 5: quadratic_cons_to_load = gurobi_cons_to_load.intersection( set(self._solver_model.getQConstrs())) linear_vals = self._solver_model.getAttr("Slack", linear_cons_to_load) if self._version_major >= 5: quadratic_vals = self._solver_model.getAttr( "QCSlack", quadratic_cons_to_load) for gurobi_con, val in zip(linear_cons_to_load, linear_vals): pyomo_con = reverse_con_map[gurobi_con] if pyomo_con in self._range_constraints: lin_expr = self._solver_model.getRow(gurobi_con) for i in reversed(range(lin_expr.size())): v = lin_expr.getVar(i) if v in gurobi_range_con_vars: Us_ = v.X Ls_ = v.UB - v.X if Us_ > Ls_: slack[pyomo_con] = Us_ else: slack[pyomo_con] = -Ls_ break else: slack[pyomo_con] = val if self._version_major >= 5: for gurobi_con, val in zip(quadratic_cons_to_load, quadratic_vals): pyomo_con = reverse_con_map[gurobi_con] slack[pyomo_con] = val 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)
class XpressDirect(DirectSolver): def __init__(self, **kwds): if 'type' not in kwds: kwds['type'] = 'xpress_direct' super(XpressDirect, self).__init__(**kwds) self._pyomo_var_to_solver_var_map = ComponentMap() self._solver_var_to_pyomo_var_map = ComponentMap() self._pyomo_con_to_solver_con_map = dict() self._solver_con_to_pyomo_con_map = ComponentMap() self._name = None try: import xpress self._xpress = xpress self._python_api_exists = True self._version = tuple( int(k) for k in self._xpress.getversion().split('.')) self._name = "Xpress %s.%s.%s" % self._version self._version_major = self._version[0] # in versions prior to 34, xpress raised a RuntimeError, but in more # recent versions it raises a xpress.ModelError. We'll cache the appropriate # one here if self._version_major < 34: self._XpressException = RuntimeError else: self._XpressException = xpress.ModelError except ImportError: self._python_api_exists = False except Exception as e: # other forms of exceptions can be thrown by the xpress python # import. for example, a xpress.InterfaceError exception is thrown # if the Xpress license is not valid. Unfortunately, you can't # import without a license, which means we can't test for the # exception above! print("Import of xpress failed - xpress message=" + str(e) + "\n") self._python_api_exists = False self._range_constraints = set() # TODO: this isn't a limit of XPRESS, which implements an SLP # method for NLPs. But it is a limit of *this* interface self._max_obj_degree = 2 self._max_constraint_degree = 2 # There does not seem to be an easy way to get the # wallclock time out of xpress, so we will measure it # ourselves self._opt_time = None # Note: Undefined capabilites default to None self._capabilities.linear = True self._capabilities.quadratic_objective = True self._capabilities.quadratic_constraint = True self._capabilities.integer = True self._capabilities.sos1 = True self._capabilities.sos2 = True def _apply_solver(self): if not self._save_results: for block in self._pyomo_model.block_data_objects( descend_into=True, active=True): for var in block.component_data_objects( ctype=pyomo.core.base.var.Var, descend_into=False, active=True, sort=False): var.stale = True self._solver_model.setlogfile(self._log_file) if self._keepfiles: print("Solver log file: " + self.log_file) # setting a log file in xpress disables all output # this callback prints all messages to stdout if self._tee: self._solver_model.addcbmessage(_print_message, None, 0) # set xpress options # if the user specifies a 'mipgap', set it, and # set xpress's related options to 0. if self.options.mipgap is not None: self._solver_model.setControl('miprelstop', float(self.options.mipgap)) self._solver_model.setControl('miprelcutoff', 0.0) self._solver_model.setControl('mipaddcutoff', 0.0) # xpress is picky about the type which is passed # into a control. So we will infer and cast # get the xpress valid controls xp_controls = self._xpress.controls for key, option in self.options.items(): if key == 'mipgap': # handled above continue try: self._solver_model.setControl(key, option) except self._XpressException: # take another try, converting to its type # we'll wrap this in a function to raise the # xpress error contr_type = type(getattr(xp_controls, key)) if not _is_convertable(contr_type, option): raise self._solver_model.setControl(key, contr_type(option)) start_time = time.time() self._solver_model.solve() self._opt_time = time.time() - start_time self._solver_model.setlogfile('') if self._tee: self._solver_model.removecbmessage(_print_message, None) # FIXME: can we get a return code indicating if XPRESS had a significant failure? return Bunch(rc=None, log=None) def _get_expr_from_pyomo_repn(self, repn, max_degree=2): referenced_vars = ComponentSet() degree = repn.polynomial_degree() if (degree is None) or (degree > max_degree): raise DegreeError( 'XpressDirect does not support expressions of degree {0}.'. format(degree)) # NOTE: xpress's python interface only allows for expresions # with native numeric types. Others, like numpy.float64, # will cause an exception when constructing expressions if len(repn.linear_vars) > 0: referenced_vars.update(repn.linear_vars) new_expr = self._xpress.Sum( float(coef) * self._pyomo_var_to_solver_var_map[var] for coef, var in zip(repn.linear_coefs, repn.linear_vars)) else: new_expr = 0.0 for coef, (x, y) in zip(repn.quadratic_coefs, repn.quadratic_vars): new_expr += float(coef) * self._pyomo_var_to_solver_var_map[ x] * self._pyomo_var_to_solver_var_map[y] referenced_vars.add(x) referenced_vars.add(y) new_expr += repn.constant return new_expr, referenced_vars def _get_expr_from_pyomo_expr(self, expr, max_degree=2): if max_degree == 2: repn = generate_standard_repn(expr, quadratic=True) else: repn = generate_standard_repn(expr, quadratic=False) try: xpress_expr, referenced_vars = self._get_expr_from_pyomo_repn( repn, max_degree) except DegreeError as e: msg = e.args[0] msg += '\nexpr: {0}'.format(expr) raise DegreeError(msg) return xpress_expr, referenced_vars def _add_var(self, var): varname = self._symbol_map.getSymbol(var, self._labeler) vartype = self._xpress_vartype_from_var(var) if var.has_lb(): lb = value(var.lb) else: lb = -self._xpress.infinity if var.has_ub(): ub = value(var.ub) else: ub = self._xpress.infinity if var.is_fixed(): lb = value(var.value) ub = value(var.value) xpress_var = self._xpress.var(name=varname, lb=lb, ub=ub, vartype=vartype) self._solver_model.addVariable(xpress_var) ## bounds on binary variables don't seem to be set correctly ## by the method above if vartype == self._xpress.binary: if lb == ub: self._solver_model.chgbounds([xpress_var], ['B'], [lb]) else: self._solver_model.chgbounds([xpress_var, xpress_var], ['L', 'U'], [lb, ub]) self._pyomo_var_to_solver_var_map[var] = xpress_var self._solver_var_to_pyomo_var_map[xpress_var] = var self._referenced_variables[var] = 0 def _set_instance(self, model, kwds={}): self._range_constraints = set() DirectOrPersistentSolver._set_instance(self, model, kwds) self._pyomo_con_to_solver_con_map = dict() self._solver_con_to_pyomo_con_map = ComponentMap() self._pyomo_var_to_solver_var_map = ComponentMap() self._solver_var_to_pyomo_var_map = ComponentMap() try: if model.name is not None: self._solver_model = self._xpress.problem(name=model.name) else: self._solver_model = self._xpress.problem() except Exception: e = sys.exc_info()[1] msg = ("Unable to create Xpress model. " "Have you installed the Python " "bindings for Xpress?\n\n\t" + "Error message: {0}".format(e)) raise Exception(msg) self._add_block(model) 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): if self._skip_trivial_constraints: return None conname = self._symbol_map.getSymbol(con, self._labeler) if con._linear_canonical_form: xpress_expr, referenced_vars = self._get_expr_from_pyomo_repn( con.canonical_form(), self._max_constraint_degree) else: xpress_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: xpress_con = self._xpress.constraint(body=xpress_expr, sense=self._xpress.eq, rhs=value(con.lower), name=conname) elif con.has_lb() and con.has_ub(): xpress_con = self._xpress.constraint(body=xpress_expr, sense=self._xpress.range, lb=value(con.lower), ub=value(con.upper), name=conname) self._range_constraints.add(xpress_con) elif con.has_lb(): xpress_con = self._xpress.constraint(body=xpress_expr, sense=self._xpress.geq, rhs=value(con.lower), name=conname) elif con.has_ub(): xpress_con = self._xpress.constraint(body=xpress_expr, sense=self._xpress.leq, rhs=value(con.upper), name=conname) else: raise ValueError("Constraint does not have a lower " "or an upper bound: {0} \n".format(con)) self._solver_model.addConstraint(xpress_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_map[con] = xpress_con self._solver_con_to_pyomo_con_map[xpress_con] = 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("Solver does not support SOS " "level {0} constraints".format(level)) xpress_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) xpress_vars.append(self._pyomo_var_to_solver_var_map[v]) self._referenced_variables[v] += 1 weights.append(w) xpress_con = self._xpress.sos(xpress_vars, weights, level, conname) self._solver_model.addSOS(xpress_con) self._pyomo_con_to_solver_con_map[con] = xpress_con self._solver_con_to_pyomo_con_map[xpress_con] = con def _xpress_vartype_from_var(self, var): """ This function takes a pyomo variable and returns the appropriate xpress variable type :param var: pyomo.core.base.var.Var :return: xpress.continuous or xpress.binary or xpress.integer """ if var.is_binary(): vartype = self._xpress.binary elif var.is_integer(): vartype = self._xpress.integer elif var.is_continuous(): vartype = self._xpress.continuous else: raise ValueError( 'Variable domain type is not recognized for {0}'.format( var.domain)) return vartype 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 = self._xpress.minimize elif obj.sense == maximize: sense = self._xpress.maximize else: raise ValueError('Objective sense is not recognized: {0}'.format( obj.sense)) xpress_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(xpress_expr, sense=sense) self._objective = obj self._vars_referenced_by_obj = referenced_vars def _postsolve(self): # the only suffixes that we extract from XPRESS 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 xpress_direct solver plugin cannot extract solution suffix=" + suffix) xprob = self._solver_model xp = self._xpress xprob_attrs = xprob.attributes ## XPRESS's status codes depend on this ## (number of integer vars > 0) or (number of special order sets > 0) is_mip = (xprob_attrs.mipents > 0) or (xprob_attrs.sets > 0) if is_mip: 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 = self._name self.results.solver.wallclock_time = self._opt_time if is_mip: status = xprob_attrs.mipstatus mip_sols = xprob_attrs.mipsols if status == xp.mip_not_loaded: self.results.solver.status = SolverStatus.aborted self.results.solver.termination_message = "Model is not loaded; no solution information is available." self.results.solver.termination_condition = TerminationCondition.error soln.status = SolutionStatus.unknown #no MIP solution, first LP did not solve, second LP did, third search started but incomplete elif status == xp.mip_lp_not_optimal \ or status == xp.mip_lp_optimal \ or status == xp.mip_no_sol_found: 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 == xp.mip_solution: # some solution available self.results.solver.status = SolverStatus.warning self.results.solver.termination_message = "Unable to satisfy optimality tolerances; a sub-optimal " \ "solution is available." self.results.solver.termination_condition = TerminationCondition.other soln.status = SolutionStatus.feasible elif status == xp.mip_infeas: # MIP proven 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 == xp.mip_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 == xp.mip_unbounded and mip_sols > 0: self.results.solver.status = SolverStatus.warning self.results.solver.termination_message = "LP relaxation was proven to be unbounded, " \ "but a solution is available." self.results.solver.termination_condition = TerminationCondition.unbounded soln.status = SolutionStatus.unbounded elif status == xp.mip_unbounded and mip_sols <= 0: self.results.solver.status = SolverStatus.warning self.results.solver.termination_message = "LP relaxation was proven to be unbounded." self.results.solver.termination_condition = TerminationCondition.unbounded soln.status = SolutionStatus.unbounded else: self.results.solver.status = SolverStatus.error self.results.solver.termination_message = \ ("Unhandled Xpress solve status " "("+str(status)+")") self.results.solver.termination_condition = TerminationCondition.error soln.status = SolutionStatus.error else: ## an LP, we'll check the lpstatus status = xprob_attrs.lpstatus if status == xp.lp_unstarted: self.results.solver.status = SolverStatus.aborted self.results.solver.termination_message = "Model is not loaded; no solution information is available." self.results.solver.termination_condition = TerminationCondition.error soln.status = SolutionStatus.unknown elif status == xp.lp_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 == xp.lp_infeas: 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 == xp.lp_cutoff: self.results.solver.status = SolverStatus.ok self.results.solver.termination_message = "Optimal objective for model was proven to be worse than the " \ "cutoff value specified; a solution is available." self.results.solver.termination_condition = TerminationCondition.minFunctionValue soln.status = SolutionStatus.optimal elif status == xp.lp_unfinished: 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 elif status == xp.lp_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 == xp.lp_cutoff_in_dual: self.results.solver.status = SolverStatus.ok self.results.solver.termination_message = "Xpress reported the LP was cutoff in the dual." self.results.solver.termination_condition = TerminationCondition.minFunctionValue soln.status = SolutionStatus.optimal elif status == xp.lp_unsolved: self.results.solver.status = SolverStatus.error self.results.solver.termination_message = "Optimization was terminated due to unrecoverable numerical " \ "difficulties." self.results.solver.termination_condition = TerminationCondition.error soln.status = SolutionStatus.error elif status == xp.lp_nonconvex: self.results.solver.status = SolverStatus.error self.results.solver.termination_message = "Optimization was terminated because nonconvex quadratic data " \ "were found." self.results.solver.termination_condition = TerminationCondition.error soln.status = SolutionStatus.error else: self.results.solver.status = SolverStatus.error self.results.solver.termination_message = \ ("Unhandled Xpress solve status " "("+str(status)+")") self.results.solver.termination_condition = TerminationCondition.error soln.status = SolutionStatus.error self.results.problem.name = xprob_attrs.matrixname if xprob_attrs.objsense == 1.0: self.results.problem.sense = minimize elif xprob_attrs.objsense == -1.0: self.results.problem.sense = maximize else: raise RuntimeError( 'Unrecognized Xpress objective sense: {0}'.format( xprob_attrs.objsense)) self.results.problem.upper_bound = None self.results.problem.lower_bound = None if not is_mip: #LP or continuous problem try: self.results.problem.upper_bound = xprob_attrs.lpobjval self.results.problem.lower_bound = xprob_attrs.lpobjval except (self._XpressException, AttributeError): pass elif xprob_attrs.objsense == 1.0: # minimizing MIP try: self.results.problem.upper_bound = xprob_attrs.mipbestobjval except (self._XpressException, AttributeError): pass try: self.results.problem.lower_bound = xprob_attrs.bestbound except (self._XpressException, AttributeError): pass elif xprob_attrs.objsense == -1.0: # maximizing MIP try: self.results.problem.upper_bound = xprob_attrs.bestbound except (self._XpressException, AttributeError): pass try: self.results.problem.lower_bound = xprob_attrs.mipbestobjval except (self._XpressException, AttributeError): pass else: raise RuntimeError( 'Unrecognized xpress objective sense: {0}'.format( xprob_attrs.objsense)) try: soln.gap = self.results.problem.upper_bound - self.results.problem.lower_bound except TypeError: soln.gap = None self.results.problem.number_of_constraints = xprob_attrs.rows + xprob_attrs.sets + xprob_attrs.qconstraints self.results.problem.number_of_nonzeros = xprob_attrs.elems self.results.problem.number_of_variables = xprob_attrs.cols self.results.problem.number_of_integer_variables = xprob_attrs.mipents self.results.problem.number_of_continuous_variables = xprob_attrs.cols - xprob_attrs.mipents self.results.problem.number_of_objectives = 1 self.results.problem.number_of_solutions = xprob_attrs.mipsols if is_mip else 1 # 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 compatability. It is more efficient to set _save_results to False and use load_vars, load_duals, etc. """ if xprob_attrs.lpstatus in \ [xp.lp_optimal, xp.lp_cutoff, xp.lp_cutoff_in_dual] or \ xprob_attrs.mipsols > 0: soln_variables = soln.variable soln_constraints = soln.constraint xpress_vars = list(self._solver_var_to_pyomo_var_map.keys()) var_vals = xprob.getSolution(xpress_vars) for xpress_var, val in zip(xpress_vars, var_vals): pyomo_var = self._solver_var_to_pyomo_var_map[xpress_var] if self._referenced_variables[pyomo_var] > 0: pyomo_var.stale = False soln_variables[xpress_var.name] = {"Value": val} if extract_reduced_costs: vals = xprob.getRCost(xpress_vars) for xpress_var, val in zip(xpress_vars, vals): pyomo_var = self._solver_var_to_pyomo_var_map[ xpress_var] if self._referenced_variables[pyomo_var] > 0: soln_variables[xpress_var.name]["Rc"] = val if extract_duals or extract_slacks: xpress_cons = list( self._solver_con_to_pyomo_con_map.keys()) for con in xpress_cons: soln_constraints[con.name] = {} if extract_duals: vals = xprob.getDual(xpress_cons) for val, con in zip(vals, xpress_cons): soln_constraints[con.name]["Dual"] = val if extract_slacks: vals = xprob.getSlack(xpress_cons) for con, val in zip(xpress_cons, vals): if con in self._range_constraints: ## for xpress, the slack on a range constraint ## is based on the upper bound lb = con.lb ub = con.ub ub_s = val expr_val = ub - ub_s lb_s = lb - expr_val if abs(ub_s) > abs(lb_s): soln_constraints[con.name]["Slack"] = ub_s else: soln_constraints[con.name]["Slack"] = lb_s else: soln_constraints[con.name]["Slack"] = val elif self._load_solutions: if xprob_attrs.lpstatus == xp.lp_optimal and \ ((not is_mip) or (xprob_attrs.mipsols > 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): mipsolval = list() mipsolcol = list() for pyomo_var, xpress_var in self._pyomo_var_to_solver_var_map.items(): if pyomo_var.value is not None: mipsolval.append(value(pyomo_var)) mipsolcol.append(xpress_var) self._solver_model.addmipsol(mipsolval, mipsolcol) def _load_vars(self, vars_to_load=None): var_map = self._pyomo_var_to_solver_var_map ref_vars = self._referenced_variables if vars_to_load is None: vars_to_load = var_map.keys() xpress_vars_to_load = [ var_map[pyomo_var] for pyomo_var in vars_to_load ] vals = self._solver_model.getSolution(xpress_vars_to_load) for var, val in zip(vars_to_load, vals): if ref_vars[var] > 0: var.stale = False var.value = val 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_map ref_vars = self._referenced_variables rc = self._pyomo_model.rc if vars_to_load is None: vars_to_load = var_map.keys() xpress_vars_to_load = [ var_map[pyomo_var] for pyomo_var in vars_to_load ] vals = self._solver_model.getRCost(xpress_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 dual = self._pyomo_model.dual if cons_to_load is None: cons_to_load = con_map.keys() xpress_cons_to_load = [ con_map[pyomo_con] for pyomo_con in cons_to_load ] vals = self._solver_model.getDual(xpress_cons_to_load) for pyomo_con, val in zip(cons_to_load, vals): dual[pyomo_con] = val 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 slack = self._pyomo_model.slack if cons_to_load is None: cons_to_load = con_map.keys() xpress_cons_to_load = [ con_map[pyomo_con] for pyomo_con in cons_to_load ] vals = self._solver_model.getSlack(xpress_cons_to_load) for pyomo_con, xpress_con, val in zip(cons_to_load, xpress_cons_to_load, vals): if xpress_con in self._range_constraints: ## for xpress, the slack on a range constraint ## is based on the upper bound lb = con.lb ub = con.ub ub_s = val expr_val = ub - ub_s lb_s = lb - expr_val if abs(ub_s) > abs(lb_s): slack[pyomo_con] = ub_s else: slack[pyomo_con] = lb_s else: slack[pyomo_con] = val 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=None): """ 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)
class BasePWRelaxationData(BaseRelaxationData): def __init__(self, component): BaseRelaxationData.__init__(self, component) self._partitions = ComponentMap() """ComponentMap: var: list of float""" self._saved_partitions = [] """list of CompnentMap""" def rebuild(self): """ Remove any auto-created vars/constraints from the relaxation block and recreate it """ self.clean_partitions() BaseRelaxationData.rebuild(self) def build(self, **kwargs): self._partitions = ComponentMap() self._saved_partitions = [] BaseRelaxationData.build(self, **kwargs) def add_point(self): """ Add a point to the current partitioning. This does not rebuild the relaxation. You must call build_relaxation to rebuild the relaxation. """ raise NotImplementedError( 'This method should be implemented in the derived class.') def _add_point(self, var, value=None): if value is not None: if (pyo.value(var.lb) < value) and (value < pyo.value(var.ub)): self._partitions[var].append(value) else: e = 'The value provided to add_point was not between the variables lower \n' + \ 'and upper bounds. No point was added.' warnings.warn(e) logger.warning(e) else: self._partitions[var].append(var.value) def push_partitions(self): """ Save the current partitioning and then clear the current partitioning """ self._saved_partitions.append(self._partitions) self.clear_partitions() def clear_partitions(self): """ Delete any existing partitioning scheme. """ tmp = ComponentMap() for var, pts in self._partitions.items(): tmp[var] = [pe.value(var.lb), pe.value(var.ub)] self._partitions = tmp def pop_partitions(self): """ Use the most recently saved partitioning. """ self._partitions = self._saved_partitions.pop(-1) def clean_partitions(self): # discard any points in the partitioning that are not within the variable bounds for var, pts in self._partitions.items(): pts.sort() for var, pts in self._partitions.items(): lb = pe.value(var.lb) ub = pe.value(var.ub) if pts[0] < lb or pts[-1] > ub: pts = [v for v in pts if (lb < v < ub)] pts.insert(0, lb) pts.append(ub) self._partitions[var] = pts def is_convex(self): """ Returns True if linear underestimators do not need binaries. Otherwise, returns False. Returns ------- bool """ raise NotImplementedError( 'This method should be implemented in the derived class.') def is_concave(self): """ Returns True if linear overestimators do not need binaries. Otherwise, returns False. Returns ------- bool """ raise NotImplementedError( 'This method should be implemented in the derived class.') def add_cut(self): if not hasattr(self, '_cuts'): self._allow_changes = True self._cuts = pyo.ConstraintList() self._allow_changes = False expr = self._get_cut_expr() if expr is not None: new_con = self._cuts.add(expr) for i in self._persistent_solvers: i.add_constraint(new_con) def _get_cut_expr(self): raise NotImplementedError( 'The add_cut method is not implemented for objects of type {0}.'. format(type(self))) def get_abs_violation(self): return abs(self.get_violation()) def get_violation(self): viol = self._get_violation() if viol >= 0 and self._relaxation_side == RelaxationSide.UNDER: viol = 0 elif viol <= 0 and self._relaxation_side == RelaxationSide.OVER: viol = 0 return viol
class MosekDirect(DirectSolver): def __init__(self, **kwds): kwds['type'] = 'mosek' DirectSolver.__init__(self, **kwds) self._pyomo_var_to_solver_var_map = ComponentMap() self._solver_var_to_pyomo_var_map = ComponentMap() self._pyomo_con_to_solver_con_map = dict() self._solver_con_to_pyomo_con_map = ComponentMap() self._init() def _init(self): self._name = None try: import mosek self._mosek = mosek self._mosek_env = self._mosek.Env() self._python_api_exists = True self._version = self._mosek_env.getversion() if self._version[0] > 8: self._name = "Mosek %s.%s.%s" % self._version while len(self._version) < 3: self._version += (0, ) else: self._name = "Mosek %s.%s.%s.%s" % self._version while len(self._version) < 4: self._version += (0, ) self._version_major = self._version[0] except ImportError: self._python_api_exists = False except Exception as e: print("Import of mosek failed - mosek message=" + str(e) + "\n") self._python_api_exists = False self._range_constraints = set() self._max_obj_degree = 2 self._max_constraint_degree = 2 self._termcode = None # Note: Undefined capabilites default to None self._capabilities.linear = True self._capabilities.quadratic_objective = True self._capabilities.quadratic_constraint = True self._capabilities.integer = True self._capabilities.sos1 = False self._capabilities.sos2 = False @staticmethod def license_is_valid(): """ Runs a check for a valid Mosek license. Returns False if Mosek fails to run on a trivial test case. """ try: import mosek except ImportError: return False try: mosek.Env().Task(0, 0).optimize() except mosek.Error: return False return True def _apply_solver(self): if not self._save_results: for block in self._pyomo_model.block_data_objects( descend_into=True, active=True): for var in block.component_data_objects( ctype=pyomo.core.base.var.Var, descend_into=False, active=True, sort=False): var.stale = True if self._tee: def _process_stream(msg): sys.stdout.write(msg) sys.stdout.flush() self._solver_model.set_Stream(self._mosek.streamtype.log, _process_stream) if self._keepfiles: print("Solver log file: " + self._log_file) for key, option in self.options.items(): param = self._mosek try: for sub_key in key.split('.'): param = getattr(param, sub_key) except (TypeError, AttributeError): raise if 'sparam' in key.split('.'): self._solver_model.putstrparam(param, option) else: if 'iparam' in key.split('.'): self._solver_model.putintparam(param, option) elif 'dparam' in key.split('.'): self._solver_model.putdouparam(param, option) else: raise AttributeError( "Unknown parameter type. Type sparam, iparam or dparam expected." ) self._termcode = self._solver_model.optimize() self._solver_model.solutionsummary(self._mosek.streamtype.msg) # FIXME: can we get a return code indicating if Mosek had a significant failure? return Bunch(rc=None, log=None) def _get_expr_from_pyomo_repn(self, repn, max_degree=2): referenced_vars = ComponentSet() degree = repn.polynomial_degree() if (degree is None) or (degree > max_degree): raise DegreeError( 'Mosek does not support expressions of degree {0}.'.format( degree)) # if len(repn.linear_vars) > 0: referenced_vars.update(repn.linear_vars) indexes = [] [ indexes.append(self._pyomo_var_to_solver_var_map[i]) for i in repn.linear_vars ] new_expr = [list(repn.linear_coefs), indexes, repn.constant] qsubi = [] qsubj = [] qval = [] for i, v in enumerate(repn.quadratic_vars): x, y = v qsubj.append(self._pyomo_var_to_solver_var_map[x]) qsubi.append(self._pyomo_var_to_solver_var_map[y]) qval.append(repn.quadratic_coefs[i] * ((qsubi == qsubj) + 1)) referenced_vars.add(x) referenced_vars.add(y) new_expr.extend([qval, qsubi, qsubj]) return new_expr, referenced_vars def _get_expr_from_pyomo_expr(self, expr, max_degree=2): if max_degree == 2: repn = generate_standard_repn(expr, quadratic=True) else: repn = generate_standard_repn(expr, quadratic=False) try: mosek_expr, referenced_vars = self._get_expr_from_pyomo_repn( repn, max_degree) except DegreeError as e: msg = e.args[0] msg += '\nexpr: {0}'.format(expr) raise DegreeError(msg) return mosek_expr, referenced_vars def _add_var(self, var): varname = self._symbol_map.getSymbol(var, self._labeler) vtype = self._mosek_vtype_from_var(var) if var.has_lb(): lb = value(var.lb) else: lb = '0' if var.has_ub(): ub = value(var.ub) else: ub = '0' bound_type = self.set_var_boundtype(var, ub, lb) self._solver_model.appendvars(1) index = self._solver_model.getnumvar() - 1 self._solver_model.putvarbound(index, bound_type, float(lb), float(ub)) self._solver_model.putvartype(index, vtype) self._solver_model.putvarname(index, varname) self._pyomo_var_to_solver_var_map[var] = index self._solver_var_to_pyomo_var_map[index] = var self._referenced_variables[var] = 0 def _set_instance(self, model, kwds={}): self._range_constraints = set() DirectOrPersistentSolver._set_instance(self, model, kwds) self._pyomo_con_to_solver_con_map = dict() self._solver_con_to_pyomo_con_map = ComponentMap() self._pyomo_var_to_solver_var_map = ComponentMap() self._solver_var_to_pyomo_var_map = ComponentMap() self._whichsol = getattr(self._mosek.soltype, kwds.pop('soltype', 'bas')) try: self._solver_model = self._mosek_env.Task(0, 0) except Exception: e = sys.exc_info()[1] msg = ("Unable to create Mosek Task. " "Have you installed the Python " "bindings for Mosek?\n\n\t" + "Error message: {0}".format(e)) raise Exception(msg) self._add_block(model) 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): if self._skip_trivial_constraints: return None conname = self._symbol_map.getSymbol(con, self._labeler) if con._linear_canonical_form: mosek_expr, referenced_vars = self._get_expr_from_pyomo_repn( con.canonical_form(), self._max_constraint_degree) else: mosek_expr, referenced_vars = self._get_expr_from_pyomo_expr( con.body, self._max_constraint_degree) self._solver_model.appendcons(1) con_index = self._solver_model.getnumcon() - 1 con_type, ub, lb = self.set_con_bounds(con, mosek_expr[2]) 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)) self._solver_model.putarow(con_index, mosek_expr[1], mosek_expr[0]) self._solver_model.putqconk(con_index, mosek_expr[4], mosek_expr[5], mosek_expr[3]) self._solver_model.putconbound(con_index, con_type, lb, ub) self._solver_model.putconname(con_index, conname) for var in referenced_vars: self._referenced_variables[var] += 1 self._vars_referenced_by_con[con] = referenced_vars self._pyomo_con_to_solver_con_map[con] = con_index self._solver_con_to_pyomo_con_map[con_index] = con def _mosek_vtype_from_var(self, var): """ This function takes a pyomo variable and returns the appropriate mosek variable type :param var: pyomo.core.base.var.Var :return: mosek.variabletype.type_int or mosek.variabletype.type_cont """ if var.is_integer() or var.is_binary(): vtype = self._mosek.variabletype.type_int elif var.is_continuous(): vtype = self._mosek.variabletype.type_cont else: raise ValueError( 'Variable domain type is not recognized for {0}'.format( var.domain)) return vtype def set_var_boundtype(self, var, ub, lb): if var.is_fixed(): return self._mosek.boundkey.fx elif ub != '0' and lb != '0': return self._mosek.boundkey.ra elif ub == '0' and lb == '0': return self._mosek.boundkey.fr elif ub != '0' and lb == '0': return self._mosek.boundkey.up return self._mosek.boundkey.lo def set_con_bounds(self, con, constant): if con.equality: ub = value(con.upper) - constant lb = value(con.lower) - constant con_type = self._mosek.boundkey.fx elif con.has_lb() and con.has_ub(): ub = value(con.upper) - constant lb = value(con.lower) - constant con_type = self._mosek.boundkey.ra elif con.has_lb(): ub = 0 lb = value(con.lower) - constant con_type = self._mosek.boundkey.lo elif con.has_ub(): ub = value(con.upper) - constant lb = 0 con_type = self._mosek.boundkey.up else: ub = 0 lb = 0 con_type = self._mosek.boundkey.fr return con_type, ub, lb 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: self._solver_model.putobjsense(self._mosek.objsense.minimize) elif obj.sense == maximize: self._solver_model.putobjsense(self._mosek.objsense.maximize) else: raise ValueError('Objective sense is not recognized: {0}'.format( obj.sense)) mosek_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 for i, j in enumerate(mosek_expr[1]): self._solver_model.putcj(j, mosek_expr[0][i]) self._solver_model.putqobj(mosek_expr[4], mosek_expr[5], mosek_expr[3]) self._solver_model.putcfix(mosek_expr[2]) self._objective = obj self._vars_referenced_by_obj = referenced_vars def _postsolve(self): 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 mosek solver plugin cannot extract solution suffix=" + suffix) msk_task = self._solver_model msk = self._mosek itr_soltypes = [msk.problemtype.qo, msk.problemtype.qcqo] if (msk_task.getnumintvar() >= 1): self._whichsol = msk.soltype.itg 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 elif (msk_task.getprobtype() in itr_soltypes): self._whichsol = msk.soltype.itr whichsol = self._whichsol sol_status = msk_task.getsolsta(whichsol) pro_status = msk_task.getprosta(whichsol) self.results = SolverResults() soln = Solution() self.results.solver.name = self._name self.results.solver.wallclock_time = msk_task.getdouinf( msk.dinfitem.optimizer_time) SOLSTA_MAP = { msk.solsta.unknown: 'unknown', msk.solsta.optimal: 'optimal', msk.solsta.prim_and_dual_feas: 'pd_feas', msk.solsta.prim_feas: 'p_feas', msk.solsta.dual_feas: 'd_feas', msk.solsta.prim_infeas_cer: 'p_infeas', msk.solsta.dual_infeas_cer: 'd_infeas', msk.solsta.prim_illposed_cer: 'p_illposed', msk.solsta.dual_illposed_cer: 'd_illposed', msk.solsta.integer_optimal: 'optimal' } PROSTA_MAP = { msk.prosta.unknown: 'unknown', msk.prosta.prim_and_dual_feas: 'pd_feas', msk.prosta.prim_feas: 'p_feas', msk.prosta.dual_feas: 'd_feas', msk.prosta.prim_infeas: 'p_infeas', msk.prosta.dual_infeas: 'd_infeas', msk.prosta.prim_and_dual_feas: 'pd_infeas', msk.prosta.ill_posed: 'illposed', msk.prosta.prim_infeas_or_unbounded: 'p_inf_unb' } if self._version_major < 9: SOLSTA_OLD = { msk.solsta.near_optimal: 'optimal', msk.solsta.near_integer_optimal: 'optimal', msk.solsta.near_prim_feas: 'p_feas', msk.solsta.near_dual_feas: 'd_feas', msk.solsta.near_prim_and_dual_feas: 'pd_feas', msk.solsta.near_prim_infeas_cer: 'p_infeas', msk.solsta.near_dual_infeas_cer: 'd_infeas' } PROSTA_OLD = { msk.prosta.near_prim_and_dual_feas: 'pd_feas', msk.prosta.near_prim_feas: 'p_feas', msk.prosta.near_dual_feas: 'd_feas' } SOLSTA_MAP.update(SOLSTA_OLD) PROSTA_MAP.update(PROSTA_OLD) if self._termcode == msk.rescode.ok: self.results.solver.status = SolverStatus.ok self.results.solver.termination_message = "" elif self._termcode == msk.rescode.trm_max_iterations: self.results.solver.status = SolverStatus.ok self.results.solver.termination_message = "Optimization terminated because the total number " \ "iterations performed exceeded the value specified in the " \ "IterationLimit parameter." self.results.solver.termination_condition = TerminationCondition.maxIterations soln.status = SolutionStatus.stoppedByLimit elif self._termcode == msk.rescode.trm_max_time: self.results.solver.status = SolverStatus.ok self.results.solver.termination_message = "Optimization terminated because the time expended exceeded " \ "the value specified in the TimeLimit parameter." self.results.solver.termination_condition = TerminationCondition.maxTimeLimit soln.status = SolutionStatus.stoppedByLimit elif self._termcode == msk.rescode.trm_user_callback: self.results.solver.status = SolverStatus.Aborted self.results.solver.termination_message = "Optimization terminated because of the user callback " self.results.solver.termination_condition = TerminationCondition.userInterrupt soln.status = SolutionStatus.unknown elif self._termcode in [ msk.rescode.trm_mio_num_relaxs, msk.rescode.trm_mio_num_branches, msk.rescode.trm_num_max_num_int_solutions ]: self.results.solver.status = SolverStatus.ok self.results.solver.termination_message = "Optimization terminated because maximum number of relaxations" \ " / branches / integer solutions exceeded " \ "the value specified in the TimeLimit parameter." self.results.solver.termination_condition = TerminationCondition.maxEvaluations soln.status = SolutionStatus.stoppedByLimit else: self.results.solver.termination_message = " Optimization terminated %s response code." \ "Check Mosek response code documentation for further explanation." % self._termcode self.results.solver.termination_condition = TerminationCondition.unknown if SOLSTA_MAP[sol_status] == 'unknown': self.results.solver.status = SolverStatus.warning self.results.solver.termination_message += " Unknown solution status." self.results.solver.Message = self.results.solver.termination_message self.results.solver.termination_condition = TerminationCondition.unknown soln.status = SolutionStatus.unknown if PROSTA_MAP[pro_status] == 'd_infeas': self.results.solver.status = SolverStatus.warning self.results.solver.termination_message += " Problem proven to be dual infeasible" self.results.solver.Message = self.results.solver.termination_message self.results.solver.termination_condition = TerminationCondition.unbounded soln.status = SolutionStatus.unbounded elif PROSTA_MAP[pro_status] == 'p_infeas': self.results.solver.status = SolverStatus.warning self.results.solver.termination_message += " Problem proven to be primal infeasible." self.results.solver.Message = self.results.solver.termination_message self.results.solver.termination_condition = TerminationCondition.infeasible soln.status = SolutionStatus.infeasible elif PROSTA_MAP[pro_status] == 'pd_infeas': self.results.solver.status = SolverStatus.warning self.results.solver.termination_message += " Problem proven to be primal and dual infeasible." self.results.solver.Message = self.results.solver.termination_message self.results.solver.termination_condition = TerminationCondition.infeasible soln.status = SolutionStatus.infeasible elif PROSTA_MAP[pro_status] == 'p_inf_unb': self.results.solver.status = SolverStatus.warning self.results.solver.termination_message += " Problem proven to be infeasible or unbounded." self.results.solver.Message = self.results.solver.termination_message self.results.solver.termination_condition = TerminationCondition.infeasibleOrUnbounded soln.status = SolutionStatus.unsure if SOLSTA_MAP[sol_status] == 'optimal': self.results.solver.status = SolverStatus.ok self.results.solver.termination_message += " Model was solved to optimality, " \ "and an optimal solution is available." self.results.solver.termination_condition = TerminationCondition.optimal soln.status = SolutionStatus.optimal elif SOLSTA_MAP[sol_status] == 'pd_feas': self.results.solver.status = SolverStatus.ok self.results.solver.termination_message += " The solution is both primal and dual feasible" self.results.solver.termination_condition = TerminationCondition.feasible soln.status = SolutionStatus.feasible elif SOLSTA_MAP[sol_status] == 'p_feas': self.results.solver.status = SolverStatus.ok self.results.solver.termination_message += " Primal feasible solution is available." self.results.solver.termination_condition = TerminationCondition.feasible soln.status = SolutionStatus.feasible elif SOLSTA_MAP[sol_status] == 'd_feas': self.results.solver.status = SolverStatus.ok self.results.solver.termination_message += " Dual feasible solution is available." self.results.solver.termination_condition = TerminationCondition.feasible soln.status = SolutionStatus.feasible elif SOLSTA_MAP[sol_status] == 'd_infeas': self.results.solver.status = SolverStatus.warning self.results.solver.termination_message += " The solution is dual infeasible." self.results.solver.Message = self.results.solver.termination_message self.results.solver.termination_condition = TerminationCondition.unbounded soln.status = SolutionStatus.infeasible elif SOLSTA_MAP[sol_status] == 'p_infeas': self.results.solver.status = SolverStatus.warning self.results.solver.termination_message += " The solution is primal infeasible." self.results.solver.Message = self.results.solver.termination_message self.results.solver.termination_condition = TerminationCondition.infeasible soln.status = SolutionStatus.infeasible self.results.problem.name = msk_task.gettaskname() if msk_task.getobjsense() == msk.objsense.minimize: self.results.problem.sense = minimize elif msk_task.getobjsense() == msk.objsense.maximize: self.results.problem.sense = maximize else: raise RuntimeError( 'Unrecognized Mosek objective sense: {0}'.format( msk_task.getobjname())) self.results.problem.upper_bound = None self.results.problem.lower_bound = None if msk_task.getnumintvar() == 0: try: if msk_task.getobjsense() == msk.objsense.minimize: self.results.problem.upper_bound = msk_task.getprimalobj( whichsol) self.results.problem.lower_bound = msk_task.getdualobj( whichsol) elif msk_task.getobjsense() == msk.objsense.maximize: self.results.problem.upper_bound = msk_task.getprimalobj( whichsol) self.results.problem.lower_bound = msk_task.getdualobj( whichsol) except (msk.MosekException, AttributeError): pass elif msk_task.getobjsense() == msk.objsense.minimize: # minimizing try: self.results.problem.upper_bound = msk_task.getprimalobj( whichsol) except (msk.MosekException, AttributeError): pass try: self.results.problem.lower_bound = msk_task.getdouinf( msk.dinfitem.mio_obj_bound) except (msk.MosekException, AttributeError): pass elif msk_task.getobjsense() == msk.objsense.maximize: # maximizing try: self.results.problem.upper_bound = msk_task.getdouinf( msk.dinfitem.mio_obj_bound) except (msk.MosekException, AttributeError): pass try: self.results.problem.lower_bound = msk_task.getprimalobj( whichsol) except (msk.MosekException, AttributeError): pass else: raise RuntimeError( 'Unrecognized Mosek objective sense: {0}'.format( msk_task.getobjsense())) try: soln.gap = self.results.problem.upper_bound - self.results.problem.lower_bound except TypeError: soln.gap = None self.results.problem.number_of_constraints = msk_task.getnumcon() self.results.problem.number_of_nonzeros = msk_task.getnumanz() self.results.problem.number_of_variables = msk_task.getnumvar() self.results.problem.number_of_integer_variables = msk_task.getnumintvar( ) self.results.problem.number_of_continuous_variables = msk_task.getnumvar() - \ msk_task.getnumintvar() self.results.problem.number_of_objectives = 1 self.results.problem.number_of_solutions = 1 # 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 compatability. It is more efficient to set _save_results to False and use load_vars, load_duals, etc. """ if self.results.problem.number_of_solutions > 0: soln_variables = soln.variable soln_constraints = soln.constraint mosek_vars = list(range(msk_task.getnumvar())) mosek_vars = list( set(mosek_vars).intersection( set(self._pyomo_var_to_solver_var_map.values()))) var_vals = [0.0] * len(mosek_vars) self._solver_model.getxx(whichsol, var_vals) names = [] for i in mosek_vars: names.append(msk_task.getvarname(i)) for mosek_var, val, name in zip(mosek_vars, var_vals, names): pyomo_var = self._solver_var_to_pyomo_var_map[mosek_var] if self._referenced_variables[pyomo_var] > 0: pyomo_var.stale = False soln_variables[name] = {"Value": val} if extract_reduced_costs: vals = [0.0] * len(mosek_vars) msk_task.getreducedcosts(whichsol, 0, len(mosek_vars), vals) for mosek_var, val, name in zip(mosek_vars, vals, names): pyomo_var = self._solver_var_to_pyomo_var_map[ mosek_var] if self._referenced_variables[pyomo_var] > 0: soln_variables[name]["Rc"] = val if extract_duals or extract_slacks: mosek_cons = list(range(msk_task.getnumcon())) con_names = [] for con in mosek_cons: con_names.append(msk_task.getconname(con)) for name in con_names: soln_constraints[name] = {} if extract_duals: vals = [0.0] * msk_task.getnumcon() msk_task.gety(whichsol, vals) for val, name in zip(vals, con_names): soln_constraints[name]["Dual"] = val if extract_slacks: Ax = [0] * len(mosek_cons) msk_task.getxc(self._whichsol, Ax) for con, name in zip(mosek_cons, con_names): Us = Ls = 0 bk, lb, ub = msk_task.getconbound(con) if bk in [ msk.boundkey.fx, msk.boundkey.ra, msk.boundkey.up ]: Us = ub - Ax[con] if bk in [ msk.boundkey.fx, msk.boundkey.ra, msk.boundkey.lo ]: Ls = Ax[con] - lb if Us > Ls: soln_constraints[name]["Slack"] = Us else: soln_constraints[name]["Slack"] = -Ls elif self._load_solutions: if self.results.problem.number_of_solutions > 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): for pyomo_var, mosek_var in self._pyomo_var_to_solver_var_map.items(): if pyomo_var.value is not None: for solType in self._mosek.soltype._values: self._solver_model.putxxslice(solType, mosek_var, mosek_var + 1, [(pyomo_var.value)]) def _load_vars(self, vars_to_load=None): var_map = self._pyomo_var_to_solver_var_map ref_vars = self._referenced_variables if vars_to_load is None: vars_to_load = var_map.keys() mosek_vars_to_load = [var_map[pyomo_var] for pyomo_var in vars_to_load] var_vals = [0.0] * len(mosek_vars_to_load) self._solver_model.getxx(self._whichsol, var_vals) for var, val in zip(vars_to_load, var_vals): if ref_vars[var] > 0: var.stale = False var.value = val 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_map ref_vars = self._referenced_variables rc = self._pyomo_model.rc if vars_to_load is None: vars_to_load = var_map.keys() mosek_vars_to_load = [var_map[pyomo_var] for pyomo_var in vars_to_load] vals = [0.0] * len(mosek_vars_to_load) self._solver_model.getreducedcosts(self._whichsol, 0, len(mosek_vars_to_load), vals) 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 if cons_to_load is None: mosek_cons_to_load = range(self._solver_model.getnumcon()) else: mosek_cons_to_load = set( [con_map[pyomo_con] for pyomo_con in cons_to_load]) vals = [0.0] * self._solver_model.getnumcon() self._solver_model.gety(self._whichsol, vals) for mosek_con, val in zip(mosek_cons_to_load, vals): pyomo_con = reverse_con_map[mosek_con] dual[pyomo_con] = val 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 msk = self._mosek if cons_to_load is None: mosek_cons_to_load = range(self._solver_model.getnumcon()) else: mosek_cons_to_load = set( [con_map[pyomo_con] for pyomo_con in cons_to_load]) Ax = [0] * len(mosek_cons_to_load) self._solver_model.getxc(self._whichsol, Ax) for con in mosek_cons_to_load: pyomo_con = reverse_con_map[con] Us = Ls = 0 bk, lb, ub = self._solver_model.getconbound(con) if bk in [msk.boundkey.fx, msk.boundkey.ra, msk.boundkey.up]: Us = ub - Ax[con] if bk in [msk.boundkey.fx, msk.boundkey.ra, msk.boundkey.lo]: Ls = Ax[con] - lb if Us > Ls: slack[pyomo_con] = Us else: slack[pyomo_con] = -Ls 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)
def fbbt_con(con, deactivate_satisfied_constraints=False, integer_tol=1e-5, infeasible_tol=1e-8): """ Feasibility based bounds tightening for a constraint. This function attempts to improve the bounds of each variable in the constraint based on the bounds of the constraint and the bounds of the other variables in the constraint. For example: >>> import pyomo.environ as pe >>> from pyomo.contrib.fbbt.fbbt import fbbt >>> m = pe.ConcreteModel() >>> m.x = pe.Var(bounds=(-1,1)) >>> m.y = pe.Var(bounds=(-2,2)) >>> m.z = pe.Var() >>> m.c = pe.Constraint(expr=m.x*m.y + m.z == 1) >>> fbbt(m.c) >>> print(m.z.lb, m.z.ub) -1.0 3.0 Parameters ---------- con: pyomo.core.base.constraint.Constraint constraint on which to perform fbbt deactivate_satisfied_constraints: bool If deactivate_satisfied_constraints is True and the constraint is always satisfied, then the constranit will be deactivated integer_tol: float If the lower bound computed on a binary variable is less than or equal to integer_tol, then the lower bound is left at 0. Otherwise, the lower bound is increased to 1. If the upper bound computed on a binary variable is greater than or equal to 1-integer_tol, then the upper bound is left at 1. Otherwise the upper bound is decreased to 0. infeasible_tol: float If the bounds computed on the body of a constraint violate the bounds of the constraint by more than infeasible_tol, then the constraint is considered infeasible and an exception is raised. Returns ------- new_var_bounds: ComponentMap A ComponentMap mapping from variables a tuple containing the lower and upper bounds, respectively, computed from FBBT. """ if not con.active: return ComponentMap() bnds_dict = ComponentMap( ) # a dictionary to store the bounds of every node in the tree # a walker to propagate bounds from the variables to the root visitorA = _FBBTVisitorLeafToRoot(bnds_dict) visitorA.dfs_postorder_stack(con.body) # Now we need to replace the bounds in bnds_dict for the root # node with the bounds on the constraint (if those bounds are # better). _lb = value(con.lower) _ub = value(con.upper) if _lb is None: _lb = -math.inf if _ub is None: _ub = math.inf lb, ub = bnds_dict[con.body] # check if the constraint is infeasible if lb > _ub + infeasible_tol or ub < _lb - infeasible_tol: raise InfeasibleConstraintException( 'Detected an infeasible constraint during FBBT: {0}'.format( str(con))) # check if the constraint is always satisfied if deactivate_satisfied_constraints: if lb >= _lb and ub <= _ub: con.deactivate() if _lb > lb: lb = _lb if _ub < ub: ub = _ub bnds_dict[con.body] = (lb, ub) # Now, propagate bounds back from the root to the variables visitorB = _FBBTVisitorRootToLeaf(bnds_dict, integer_tol=integer_tol) visitorB.dfs_postorder_stack(con.body) new_var_bounds = ComponentMap() for _node, _bnds in bnds_dict.items(): if _node.__class__ in nonpyomo_leaf_types: continue if _node.is_variable_type(): lb, ub = bnds_dict[_node] if lb == -math.inf: lb = None if ub == math.inf: ub = None new_var_bounds[_node] = (lb, ub) return new_var_bounds
def fbbt_con(con, deactivate_satisfied_constraints=False, integer_tol=1e-5, infeasible_tol=1e-8): """ Feasibility based bounds tightening for a constraint. This function attempts to improve the bounds of each variable in the constraint based on the bounds of the constraint and the bounds of the other variables in the constraint. For example: >>> import pyomo.environ as pe >>> from pyomo.contrib.fbbt.fbbt import fbbt >>> m = pe.ConcreteModel() >>> m.x = pe.Var(bounds=(-1,1)) >>> m.y = pe.Var(bounds=(-2,2)) >>> m.z = pe.Var() >>> m.c = pe.Constraint(expr=m.x*m.y + m.z == 1) >>> fbbt(m.c) >>> print(m.z.lb, m.z.ub) -1.0 3.0 Parameters ---------- con: pyomo.core.base.constraint.Constraint constraint on which to perform fbbt deactivate_satisfied_constraints: bool If deactivate_satisfied_constraints is True and the constraint is always satisfied, then the constranit will be deactivated integer_tol: float If the lower bound computed on a binary variable is less than or equal to integer_tol, then the lower bound is left at 0. Otherwise, the lower bound is increased to 1. If the upper bound computed on a binary variable is greater than or equal to 1-integer_tol, then the upper bound is left at 1. Otherwise the upper bound is decreased to 0. infeasible_tol: float If the bounds computed on the body of a constraint violate the bounds of the constraint by more than infeasible_tol, then the constraint is considered infeasible and an exception is raised. Returns ------- new_var_bounds: ComponentMap A ComponentMap mapping from variables a tuple containing the lower and upper bounds, respectively, computed from FBBT. """ if not con.active: return ComponentMap() bnds_dict = ComponentMap() # a dictionary to store the bounds of every node in the tree # a walker to propagate bounds from the variables to the root visitorA = _FBBTVisitorLeafToRoot(bnds_dict) visitorA.dfs_postorder_stack(con.body) # Now we need to replace the bounds in bnds_dict for the root # node with the bounds on the constraint (if those bounds are # better). _lb = value(con.lower) _ub = value(con.upper) if _lb is None: _lb = -math.inf if _ub is None: _ub = math.inf lb, ub = bnds_dict[con.body] # check if the constraint is infeasible if lb > _ub + infeasible_tol or ub < _lb - infeasible_tol: raise InfeasibleConstraintException('Detected an infeasible constraint during FBBT: {0}'.format(str(con))) # check if the constraint is always satisfied if deactivate_satisfied_constraints: if lb >= _lb and ub <= _ub: con.deactivate() if _lb > lb: lb = _lb if _ub < ub: ub = _ub bnds_dict[con.body] = (lb, ub) # Now, propagate bounds back from the root to the variables visitorB = _FBBTVisitorRootToLeaf(bnds_dict, integer_tol=integer_tol) visitorB.dfs_postorder_stack(con.body) new_var_bounds = ComponentMap() for _node, _bnds in bnds_dict.items(): if _node.__class__ in nonpyomo_leaf_types: continue if _node.is_variable_type(): lb, ub = bnds_dict[_node] if lb == -math.inf: lb = None if ub == math.inf: ub = None new_var_bounds[_node] = (lb, ub) return new_var_bounds
class GurobiDirect(DirectSolver): def __init__(self, **kwds): kwds['type'] = 'gurobi_direct' DirectSolver.__init__(self, **kwds) self._pyomo_var_to_solver_var_map = ComponentMap() self._solver_var_to_pyomo_var_map = ComponentMap() self._pyomo_con_to_solver_con_map = dict() self._solver_con_to_pyomo_con_map = ComponentMap() self._init() def _init(self): self._name = None try: import gurobipy self._gurobipy = gurobipy self._python_api_exists = True self._version = self._gurobipy.gurobi.version() self._name = "Gurobi %s.%s%s" % self._version while len(self._version) < 4: self._version += (0,) self._version = self._version[:4] self._version_major = self._version[0] except ImportError: self._python_api_exists = False except Exception as e: # other forms of exceptions can be thrown by the gurobi python # import. for example, a gurobipy.GurobiError exception is thrown # if all tokens for Gurobi are already in use. assuming, of # course, the license is a token license. unfortunately, you can't # import without a license, which means we can't test for the # exception above! print("Import of gurobipy failed - gurobi message=" + str(e) + "\n") self._python_api_exists = False self._range_constraints = set() self._max_obj_degree = 2 self._max_constraint_degree = 2 # Note: Undefined capabilites default to None self._capabilities.linear = True self._capabilities.quadratic_objective = True self._capabilities.quadratic_constraint = True self._capabilities.integer = True self._capabilities.sos1 = True self._capabilities.sos2 = True # fix for compatibility with pre-5.0 Gurobi if self._python_api_exists and \ (self._version_major < 5): self._max_constraint_degree = 1 self._capabilities.quadratic_constraint = False def _apply_solver(self): if not self._save_results: for block in self._pyomo_model.block_data_objects(descend_into=True, active=True): for var in block.component_data_objects(ctype=pyomo.core.base.var.Var, descend_into=False, active=True, sort=False): var.stale = True if self._tee: self._solver_model.setParam('OutputFlag', 1) else: self._solver_model.setParam('OutputFlag', 0) self._solver_model.setParam('LogFile', self._log_file) if self._keepfiles: print("Solver log file: "+self._log_file) # Options accepted by gurobi (case insensitive): # ['Cutoff', 'IterationLimit', 'NodeLimit', 'SolutionLimit', 'TimeLimit', # 'FeasibilityTol', 'IntFeasTol', 'MarkowitzTol', 'MIPGap', 'MIPGapAbs', # 'OptimalityTol', 'PSDTol', 'Method', 'PerturbValue', 'ObjScale', 'ScaleFlag', # 'SimplexPricing', 'Quad', 'NormAdjust', 'BarIterLimit', 'BarConvTol', # 'BarCorrectors', 'BarOrder', 'Crossover', 'CrossoverBasis', 'BranchDir', # 'Heuristics', 'MinRelNodes', 'MIPFocus', 'NodefileStart', 'NodefileDir', # 'NodeMethod', 'PumpPasses', 'RINS', 'SolutionNumber', 'SubMIPNodes', 'Symmetry', # 'VarBranch', 'Cuts', 'CutPasses', 'CliqueCuts', 'CoverCuts', 'CutAggPasses', # 'FlowCoverCuts', 'FlowPathCuts', 'GomoryPasses', 'GUBCoverCuts', 'ImpliedCuts', # 'MIPSepCuts', 'MIRCuts', 'NetworkCuts', 'SubMIPCuts', 'ZeroHalfCuts', 'ModKCuts', # 'Aggregate', 'AggFill', 'PreDual', 'DisplayInterval', 'IISMethod', 'InfUnbdInfo', # 'LogFile', 'PreCrush', 'PreDepRow', 'PreMIQPMethod', 'PrePasses', 'Presolve', # 'ResultFile', 'ImproveStartTime', 'ImproveStartGap', 'Threads', 'Dummy', 'OutputFlag'] for key, option in self.options.items(): # When options come from the pyomo command, all # values are string types, so we try to cast # them to a numeric value in the event that # setting the parameter fails. try: self._solver_model.setParam(key, option) except TypeError: # we place the exception handling for # checking the cast of option to a float in # another function so that we can simply # call raise here instead of except # TypeError as e / raise e, because the # latter does not preserve the Gurobi stack # trace if not _is_numeric(option): raise self._solver_model.setParam(key, float(option)) if self._version_major >= 5: for suffix in self._suffixes: if re.match(suffix, "dual"): self._solver_model.setParam(self._gurobipy.GRB.Param.QCPDual, 1) self._solver_model.optimize() self._solver_model.setParam('LogFile', 'default') # FIXME: can we get a return code indicating if Gurobi had a significant failure? return Bunch(rc=None, log=None) def _get_expr_from_pyomo_repn(self, repn, max_degree=2): referenced_vars = ComponentSet() degree = repn.polynomial_degree() if (degree is None) or (degree > max_degree): raise DegreeError('GurobiDirect does not support expressions of degree {0}.'.format(degree)) if len(repn.linear_vars) > 0: referenced_vars.update(repn.linear_vars) new_expr = self._gurobipy.LinExpr(repn.linear_coefs, [self._pyomo_var_to_solver_var_map[i] for i in repn.linear_vars]) else: new_expr = 0.0 for i,v in enumerate(repn.quadratic_vars): x,y = v new_expr += repn.quadratic_coefs[i] * self._pyomo_var_to_solver_var_map[x] * self._pyomo_var_to_solver_var_map[y] referenced_vars.add(x) referenced_vars.add(y) new_expr += repn.constant return new_expr, referenced_vars def _get_expr_from_pyomo_expr(self, expr, max_degree=2): if max_degree == 2: repn = generate_standard_repn(expr, quadratic=True) else: repn = generate_standard_repn(expr, quadratic=False) try: gurobi_expr, referenced_vars = self._get_expr_from_pyomo_repn(repn, max_degree) except DegreeError as e: msg = e.args[0] msg += '\nexpr: {0}'.format(expr) raise DegreeError(msg) return gurobi_expr, referenced_vars def _add_var(self, var): varname = self._symbol_map.getSymbol(var, self._labeler) vtype = self._gurobi_vtype_from_var(var) if var.has_lb(): lb = value(var.lb) else: lb = -self._gurobipy.GRB.INFINITY if var.has_ub(): ub = value(var.ub) else: ub = self._gurobipy.GRB.INFINITY gurobipy_var = self._solver_model.addVar(lb=lb, ub=ub, vtype=vtype, name=varname) self._pyomo_var_to_solver_var_map[var] = gurobipy_var self._solver_var_to_pyomo_var_map[gurobipy_var] = var self._referenced_variables[var] = 0 if var.is_fixed(): gurobipy_var.setAttr('lb', var.value) gurobipy_var.setAttr('ub', var.value) def _set_instance(self, model, kwds={}): self._range_constraints = set() DirectOrPersistentSolver._set_instance(self, model, kwds) self._pyomo_con_to_solver_con_map = dict() self._solver_con_to_pyomo_con_map = ComponentMap() self._pyomo_var_to_solver_var_map = ComponentMap() self._solver_var_to_pyomo_var_map = ComponentMap() try: if model.name is not None: self._solver_model = self._gurobipy.Model(model.name) else: self._solver_model = self._gurobipy.Model() except Exception: e = sys.exc_info()[1] msg = ("Unable to create Gurobi model. " "Have you installed the Python " "bindings for Gurboi?\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 Gurobi instance." % (var.name, self._pyomo_model.name,)) def _add_block(self, block): DirectOrPersistentSolver._add_block(self, block) self._solver_model.update() def _add_constraint(self, con): if not con.active: return None if is_fixed(con.body): if self._skip_trivial_constraints: return None conname = self._symbol_map.getSymbol(con, self._labeler) if con._linear_canonical_form: gurobi_expr, referenced_vars = self._get_expr_from_pyomo_repn( con.canonical_form(), self._max_constraint_degree) #elif isinstance(con, LinearCanonicalRepn): # gurobi_expr, referenced_vars = self._get_expr_from_pyomo_repn( # con, # self._max_constraint_degree) else: gurobi_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: gurobipy_con = self._solver_model.addConstr(lhs=gurobi_expr, sense=self._gurobipy.GRB.EQUAL, rhs=value(con.lower), name=conname) elif con.has_lb() and con.has_ub(): gurobipy_con = self._solver_model.addRange(gurobi_expr, value(con.lower), value(con.upper), name=conname) self._range_constraints.add(con) elif con.has_lb(): gurobipy_con = self._solver_model.addConstr(lhs=gurobi_expr, sense=self._gurobipy.GRB.GREATER_EQUAL, rhs=value(con.lower), name=conname) elif con.has_ub(): gurobipy_con = self._solver_model.addConstr(lhs=gurobi_expr, sense=self._gurobipy.GRB.LESS_EQUAL, rhs=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_map[con] = gurobipy_con self._solver_con_to_pyomo_con_map[gurobipy_con] = 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 == 1: sos_type = self._gurobipy.GRB.SOS_TYPE1 elif level == 2: sos_type = self._gurobipy.GRB.SOS_TYPE2 else: raise ValueError("Solver does not support SOS " "level {0} constraints".format(level)) gurobi_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) gurobi_vars.append(self._pyomo_var_to_solver_var_map[v]) self._referenced_variables[v] += 1 weights.append(w) gurobipy_con = self._solver_model.addSOS(sos_type, gurobi_vars, weights) self._pyomo_con_to_solver_con_map[con] = gurobipy_con self._solver_con_to_pyomo_con_map[gurobipy_con] = con def _gurobi_vtype_from_var(self, var): """ This function takes a pyomo variable and returns the appropriate gurobi variable type :param var: pyomo.core.base.var.Var :return: gurobipy.GRB.CONTINUOUS or gurobipy.GRB.BINARY or gurobipy.GRB.INTEGER """ if var.is_binary(): vtype = self._gurobipy.GRB.BINARY elif var.is_integer(): vtype = self._gurobipy.GRB.INTEGER elif var.is_continuous(): vtype = self._gurobipy.GRB.CONTINUOUS 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 = self._gurobipy.GRB.MINIMIZE elif obj.sense == maximize: sense = self._gurobipy.GRB.MAXIMIZE else: raise ValueError('Objective sense is not recognized: {0}'.format(obj.sense)) gurobi_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(gurobi_expr, sense=sense) self._objective = obj self._vars_referenced_by_obj = referenced_vars def _postsolve(self): # the only suffixes that we extract from GUROBI 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 gurobi_direct solver plugin cannot extract solution suffix="+suffix) gprob = self._solver_model grb = self._gurobipy.GRB status = gprob.Status if gprob.getAttr(self._gurobipy.GRB.Attr.IsMIP): 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 = self._name self.results.solver.wallclock_time = gprob.Runtime if status == grb.LOADED: # problem is loaded, but no solution 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 == grb.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 == grb.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 == grb.INF_OR_UNBD: 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 == grb.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 == grb.CUTOFF: self.results.solver.status = SolverStatus.aborted self.results.solver.termination_message = "Optimal objective for model was proven to be worse than the " \ "value specified in the Cutoff parameter. No solution " \ "information is available." self.results.solver.termination_condition = TerminationCondition.minFunctionValue soln.status = SolutionStatus.unknown elif status == grb.ITERATION_LIMIT: self.results.solver.status = SolverStatus.aborted self.results.solver.termination_message = "Optimization terminated because the total number of simplex " \ "iterations performed exceeded the value specified in the " \ "IterationLimit parameter." self.results.solver.termination_condition = TerminationCondition.maxIterations soln.status = SolutionStatus.stoppedByLimit elif status == grb.NODE_LIMIT: self.results.solver.status = SolverStatus.aborted self.results.solver.termination_message = "Optimization terminated because the total number of " \ "branch-and-cut nodes explored exceeded the value specified " \ "in the NodeLimit parameter" self.results.solver.termination_condition = TerminationCondition.maxEvaluations soln.status = SolutionStatus.stoppedByLimit elif status == grb.TIME_LIMIT: self.results.solver.status = SolverStatus.aborted self.results.solver.termination_message = "Optimization terminated because the time expended exceeded " \ "the value specified in the TimeLimit parameter." self.results.solver.termination_condition = TerminationCondition.maxTimeLimit soln.status = SolutionStatus.stoppedByLimit elif status == grb.SOLUTION_LIMIT: 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 SolutionLimit parameter." self.results.solver.termination_condition = TerminationCondition.unknown soln.status = SolutionStatus.stoppedByLimit elif status == grb.INTERRUPTED: 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 elif status == grb.NUMERIC: self.results.solver.status = SolverStatus.error self.results.solver.termination_message = "Optimization was terminated due to unrecoverable numerical " \ "difficulties." self.results.solver.termination_condition = TerminationCondition.error soln.status = SolutionStatus.error elif status == grb.SUBOPTIMAL: self.results.solver.status = SolverStatus.warning self.results.solver.termination_message = "Unable to satisfy optimality tolerances; a sub-optimal " \ "solution is available." self.results.solver.termination_condition = TerminationCondition.other soln.status = SolutionStatus.feasible # note that USER_OBJ_LIMIT was added in Gurobi 7.0, so it may not be present elif (status is not None) and \ (status == getattr(grb,'USER_OBJ_LIMIT',None)): self.results.solver.status = SolverStatus.aborted self.results.solver.termination_message = "User specified an objective limit " \ "(a bound on either the best objective " \ "or the best bound), and that limit has " \ "been reached. Solution is available." self.results.solver.termination_condition = TerminationCondition.other soln.status = SolutionStatus.stoppedByLimit else: self.results.solver.status = SolverStatus.error self.results.solver.termination_message = \ ("Unhandled Gurobi solve status " "("+str(status)+")") self.results.solver.termination_condition = TerminationCondition.error soln.status = SolutionStatus.error self.results.problem.name = gprob.ModelName if gprob.ModelSense == 1: self.results.problem.sense = minimize elif gprob.ModelSense == -1: self.results.problem.sense = maximize else: raise RuntimeError('Unrecognized gurobi objective sense: {0}'.format(gprob.ModelSense)) self.results.problem.upper_bound = None self.results.problem.lower_bound = None if (gprob.NumBinVars + gprob.NumIntVars) == 0: try: self.results.problem.upper_bound = gprob.ObjVal self.results.problem.lower_bound = gprob.ObjVal except (self._gurobipy.GurobiError, AttributeError): pass elif gprob.ModelSense == 1: # minimizing try: self.results.problem.upper_bound = gprob.ObjVal except (self._gurobipy.GurobiError, AttributeError): pass try: self.results.problem.lower_bound = gprob.ObjBound except (self._gurobipy.GurobiError, AttributeError): pass elif gprob.ModelSense == -1: # maximizing try: self.results.problem.upper_bound = gprob.ObjBound except (self._gurobipy.GurobiError, AttributeError): pass try: self.results.problem.lower_bound = gprob.ObjVal except (self._gurobipy.GurobiError, AttributeError): pass else: raise RuntimeError('Unrecognized gurobi objective sense: {0}'.format(gprob.ModelSense)) try: soln.gap = self.results.problem.upper_bound - self.results.problem.lower_bound except TypeError: soln.gap = None self.results.problem.number_of_constraints = gprob.NumConstrs + gprob.NumQConstrs + gprob.NumSOS self.results.problem.number_of_nonzeros = gprob.NumNZs self.results.problem.number_of_variables = gprob.NumVars self.results.problem.number_of_binary_variables = gprob.NumBinVars self.results.problem.number_of_integer_variables = gprob.NumIntVars self.results.problem.number_of_continuous_variables = gprob.NumVars - gprob.NumIntVars - gprob.NumBinVars self.results.problem.number_of_objectives = 1 self.results.problem.number_of_solutions = gprob.SolCount # 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 compatability. It is more efficient to set _save_results to False and use load_vars, load_duals, etc. """ if gprob.SolCount > 0: soln_variables = soln.variable soln_constraints = soln.constraint gurobi_vars = self._solver_model.getVars() gurobi_vars = list(set(gurobi_vars).intersection(set(self._pyomo_var_to_solver_var_map.values()))) var_vals = self._solver_model.getAttr("X", gurobi_vars) names = self._solver_model.getAttr("VarName", gurobi_vars) for gurobi_var, val, name in zip(gurobi_vars, var_vals, names): pyomo_var = self._solver_var_to_pyomo_var_map[gurobi_var] if self._referenced_variables[pyomo_var] > 0: pyomo_var.stale = False soln_variables[name] = {"Value": val} if extract_reduced_costs: vals = self._solver_model.getAttr("Rc", gurobi_vars) for gurobi_var, val, name in zip(gurobi_vars, vals, names): pyomo_var = self._solver_var_to_pyomo_var_map[gurobi_var] if self._referenced_variables[pyomo_var] > 0: soln_variables[name]["Rc"] = val if extract_duals or extract_slacks: gurobi_cons = self._solver_model.getConstrs() con_names = self._solver_model.getAttr("ConstrName", gurobi_cons) for name in con_names: soln_constraints[name] = {} if self._version_major >= 5: gurobi_q_cons = self._solver_model.getQConstrs() q_con_names = self._solver_model.getAttr("QCName", gurobi_q_cons) for name in q_con_names: soln_constraints[name] = {} if extract_duals: vals = self._solver_model.getAttr("Pi", gurobi_cons) for val, name in zip(vals, con_names): soln_constraints[name]["Dual"] = val if self._version_major >= 5: q_vals = self._solver_model.getAttr("QCPi", gurobi_q_cons) for val, name in zip(q_vals, q_con_names): soln_constraints[name]["Dual"] = val if extract_slacks: gurobi_range_con_vars = set(self._solver_model.getVars()) - set(self._pyomo_var_to_solver_var_map.values()) vals = self._solver_model.getAttr("Slack", gurobi_cons) for gurobi_con, val, name in zip(gurobi_cons, vals, con_names): pyomo_con = self._solver_con_to_pyomo_con_map[gurobi_con] if pyomo_con in self._range_constraints: lin_expr = self._solver_model.getRow(gurobi_con) for i in reversed(range(lin_expr.size())): v = lin_expr.getVar(i) if v in gurobi_range_con_vars: Us_ = v.X Ls_ = v.UB - v.X if Us_ > Ls_: soln_constraints[name]["Slack"] = Us_ else: soln_constraints[name]["Slack"] = -Ls_ break else: soln_constraints[name]["Slack"] = val if self._version_major >= 5: q_vals = self._solver_model.getAttr("QCSlack", gurobi_q_cons) for val, name in zip(q_vals, q_con_names): soln_constraints[name]["Slack"] = val elif self._load_solutions: if gprob.SolCount > 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): for pyomo_var, gurobipy_var in self._pyomo_var_to_solver_var_map.items(): if pyomo_var.value is not None: gurobipy_var.setAttr(self._gurobipy.GRB.Attr.Start, value(pyomo_var)) def _load_vars(self, vars_to_load=None): var_map = self._pyomo_var_to_solver_var_map ref_vars = self._referenced_variables if vars_to_load is None: vars_to_load = var_map.keys() gurobi_vars_to_load = [var_map[pyomo_var] for pyomo_var in vars_to_load] vals = self._solver_model.getAttr("X", gurobi_vars_to_load) for var, val in zip(vars_to_load, vals): if ref_vars[var] > 0: var.stale = False var.value = val 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_map ref_vars = self._referenced_variables rc = self._pyomo_model.rc if vars_to_load is None: vars_to_load = var_map.keys() gurobi_vars_to_load = [var_map[pyomo_var] for pyomo_var in vars_to_load] vals = self._solver_model.getAttr("Rc", gurobi_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 if cons_to_load is None: linear_cons_to_load = self._solver_model.getConstrs() if self._version_major >= 5: quadratic_cons_to_load = self._solver_model.getQConstrs() else: gurobi_cons_to_load = set([con_map[pyomo_con] for pyomo_con in cons_to_load]) linear_cons_to_load = gurobi_cons_to_load.intersection(set(self._solver_model.getConstrs())) if self._version_major >= 5: quadratic_cons_to_load = gurobi_cons_to_load.intersection(set(self._solver_model.getQConstrs())) linear_vals = self._solver_model.getAttr("Pi", linear_cons_to_load) if self._version_major >= 5: quadratic_vals = self._solver_model.getAttr("QCPi", quadratic_cons_to_load) for gurobi_con, val in zip(linear_cons_to_load, linear_vals): pyomo_con = reverse_con_map[gurobi_con] dual[pyomo_con] = val if self._version_major >= 5: for gurobi_con, val in zip(quadratic_cons_to_load, quadratic_vals): pyomo_con = reverse_con_map[gurobi_con] dual[pyomo_con] = val 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 gurobi_range_con_vars = set(self._solver_model.getVars()) - set(self._pyomo_var_to_solver_var_map.values()) if cons_to_load is None: linear_cons_to_load = self._solver_model.getConstrs() if self._version_major >= 5: quadratic_cons_to_load = self._solver_model.getQConstrs() else: gurobi_cons_to_load = set([con_map[pyomo_con] for pyomo_con in cons_to_load]) linear_cons_to_load = gurobi_cons_to_load.intersection(set(self._solver_model.getConstrs())) if self._version_major >= 5: quadratic_cons_to_load = gurobi_cons_to_load.intersection(set(self._solver_model.getQConstrs())) linear_vals = self._solver_model.getAttr("Slack", linear_cons_to_load) if self._version_major >= 5: quadratic_vals = self._solver_model.getAttr("QCSlack", quadratic_cons_to_load) for gurobi_con, val in zip(linear_cons_to_load, linear_vals): pyomo_con = reverse_con_map[gurobi_con] if pyomo_con in self._range_constraints: lin_expr = self._solver_model.getRow(gurobi_con) for i in reversed(range(lin_expr.size())): v = lin_expr.getVar(i) if v in gurobi_range_con_vars: Us_ = v.X Ls_ = v.UB - v.X if Us_ > Ls_: slack[pyomo_con] = Us_ else: slack[pyomo_con] = -Ls_ break else: slack[pyomo_con] = val if self._version_major >= 5: for gurobi_con, val in zip(quadratic_cons_to_load, quadratic_vals): pyomo_con = reverse_con_map[gurobi_con] slack[pyomo_con] = val 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)
class BasePWRelaxationData(BaseRelaxationData): def __init__(self, component): BaseRelaxationData.__init__(self, component) self._partitions = ComponentMap() # ComponentMap: var: list of float self._saved_partitions = list() # list of CompnentMap def rebuild(self, build_nonlinear_constraint=False): """ Remove any auto-created vars/constraints from the relaxation block and recreate it """ self.clean_partitions() super(BasePWRelaxationData, self).rebuild(build_nonlinear_constraint=build_nonlinear_constraint) def _set_input(self, relaxation_side=RelaxationSide.BOTH, persistent_solvers=None, use_linear_relaxation=True, large_eval_tol=math.inf): self._partitions = ComponentMap() self._saved_partitions = list() BaseRelaxationData._set_input(self, relaxation_side=relaxation_side, persistent_solvers=persistent_solvers, use_linear_relaxation=use_linear_relaxation, large_eval_tol=large_eval_tol) def add_parition_point(self): """ Add a point to the current partitioning. This does not rebuild the relaxation. You must call rebuild() to rebuild the relaxation. """ raise NotImplementedError('This method should be implemented in the derived class.') def _add_partition_point(self, var, value=None): if value is None: value = pe.value(var) # if the point is outside the variable's bounds, then it will simply get removed when clean_partitions # gets called. self._partitions[var].append(value) def push_partitions(self): """ Save the current partitioning and then clear the current partitioning """ self._saved_partitions.append(self._partitions) self.clear_partitions() def clear_partitions(self): """ Delete any existing partitioning scheme. """ tmp = ComponentMap() for var, pts in self._partitions.items(): tmp[var] = [pe.value(var.lb), pe.value(var.ub)] self._partitions = tmp def pop_partitions(self): """ Use the most recently saved partitioning. """ self._partitions = self._saved_partitions.pop(-1) def clean_partitions(self): # discard any points in the partitioning that are not within the variable bounds for var, pts in self._partitions.items(): pts.sort() for var, pts in self._partitions.items(): lb, ub = tuple(_get_bnds_list(var)) if pts[0] < lb or pts[-1] > ub: pts = [v for v in pts if (lb < v < ub)] pts.insert(0, lb) pts.append(ub) self._partitions[var] = pts def get_active_partitions(self): ans = ComponentMap() for var, pts in self._partitions.items(): val = pyo.value(var) lower = var.lb upper = var.ub for p in pts: if val >= p and p > lower: lower = p if val <= p and p < upper: upper = p ans[var] = lower, upper return ans
def _process_logical_constraints_in_logical_context(context): new_xfrm_block_name = unique_component_name(context, 'logic_to_linear') new_xfrm_block = Block(doc="Transformation objects for logic_to_linear") setattr(context, new_xfrm_block_name, new_xfrm_block) new_constrlist = new_xfrm_block.transformed_constraints = ConstraintList() new_boolvarlist = new_xfrm_block.augmented_vars = BooleanVarList() new_varlist = new_xfrm_block.augmented_vars_asbinary = VarList( domain=Binary) indicator_map = ComponentMap() cnf_statements = [] # Convert all logical constraints to CNF for logical_constraint in context.component_data_objects( ctype=LogicalConstraint, active=True): cnf_statements.extend( to_cnf(logical_constraint.body, new_boolvarlist, indicator_map)) logical_constraint.deactivate() # Associate new Boolean vars to new binary variables for bool_vardata in new_boolvarlist.values(): new_binary_vardata = new_varlist.add() bool_vardata.associate_binary_var(new_binary_vardata) # Add constraints associated with each CNF statement for cnf_statement in cnf_statements: for linear_constraint in _cnf_to_linear_constraint_list(cnf_statement): new_constrlist.add(expr=linear_constraint) # Add bigM associated with special atoms # Note: this ad-hoc reformulation may be revisited for tightness in the future. old_varlist_length = len(new_varlist) for indicator_var, special_atom in indicator_map.items(): for linear_constraint in _cnf_to_linear_constraint_list( special_atom, indicator_var, new_varlist): new_constrlist.add(expr=linear_constraint) # Previous step may have added auxiliary binaries. Associate augmented Booleans to them. num_new = len(new_varlist) - old_varlist_length list_o_vars = list(new_varlist.values()) if num_new: for binary_vardata in list_o_vars[-num_new:]: new_bool_vardata = new_boolvarlist.add() new_bool_vardata.associate_binary_var(binary_vardata) # If added components were not used, remove them. # Note: it is ok to simply delete the index_set for these components, because by # default, a new set object is generated for each [Thing]List. if len(new_constrlist) == 0: new_xfrm_block.del_component(new_constrlist.index_set()) new_xfrm_block.del_component(new_constrlist) if len(new_boolvarlist) == 0: new_xfrm_block.del_component(new_boolvarlist.index_set()) new_xfrm_block.del_component(new_boolvarlist) if len(new_varlist) == 0: new_xfrm_block.del_component(new_varlist.index_set()) new_xfrm_block.del_component(new_varlist) # If block was entirely unused, remove it if all( len(l) == 0 for l in (new_constrlist, new_boolvarlist, new_varlist)): context.del_component(new_xfrm_block)
def categorize_variables(model, initial_inputs): """Creates lists of time-only-slices of the different types of variables in a model, given knowledge of which are inputs. These lists are added as attributes to the model's namespace. Possible variable categories are: - INPUT --- Those specified by the user to be inputs - DERIVATIVE --- Those declared as Pyomo DerivativeVars, whose "state variable" is not fixed, except possibly as an initial condition - DIFFERENTIAL --- Those referenced as the "state variable" by an unfixed (except possibly as an initial condition) DerivativeVar - FIXED --- Those that are fixed at non-initial time points. These are typically disturbances, design variables, or uncertain parameters. - ALGEBRAIC --- Unfixed, time-indexed variables that are neither inputs nor referenced by an unfixed derivative. - SCALAR --- Variables unindexed by time. These could be variables that refer to a specific point in time (initial or final conditions), averages over time, or truly time-independent variables like diameter. Args: model : Model whose variables will be flattened and categorized initial_inputs : List of VarData objects that are input variables at the initial time point """ namespace = getattr(model, DynamicBase.get_namespace_name()) time = namespace.get_time() t0 = time.first() t1 = time.get_finite_elements()[1] deriv_vars = [] diff_vars = [] input_vars = [] alg_vars = [] fixed_vars = [] ic_vars = [] # Create list of time-only-slices of time indexed variables # (And list of VarData objects for scalar variables) scalar_vars, dae_vars = flatten_dae_variables(model, time) dae_map = ComponentMap([(v[t0], v) for v in dae_vars]) t0_vardata = list(dae_map.keys()) namespace.dae_vars = list(dae_map.values()) namespace.scalar_vars = \ NMPCVarGroup( list(ComponentMap([(v, v) for v in scalar_vars]).values()), index_set=None, is_scalar=True) namespace.n_scalar_vars = \ namespace.scalar_vars.n_vars input_set = ComponentSet(initial_inputs) updated_input_set = ComponentSet(initial_inputs) # Iterate over initial vardata, popping from dae map when an input, # derivative, or differential var is found. for var0 in t0_vardata: if var0 in updated_input_set: input_set.remove(var0) time_slice = dae_map.pop(var0) input_vars.append(time_slice) parent = var0.parent_component() if not isinstance(parent, DerivativeVar): continue if not time in ComponentSet(parent.get_continuousset_list()): continue index0 = var0.index() var1 = dae_map[var0][t1] index1 = var1.index() state = parent.get_state_var() if state[index1].fixed: # Assume state var is fixed everywhere, so derivative # 'isn't really' a derivative. # Should be safe to remove state from dae_map here state_slice = dae_map.pop(state[index0]) fixed_vars.append(state_slice) continue if state[index0] in input_set: # If differential variable is an input, then this DerivativeVar # is 'not really a derivative' continue deriv_slice = dae_map.pop(var0) if var1.fixed: # Assume derivative has been fixed everywhere. # Add to list of fixed variables, and don't remove its state variable. fixed_vars.append(deriv_slice) elif var0.fixed: # In this case the derivative has been used as an initial condition. # Still want to include it in the list of derivatives. ic_vars.append(deriv_slice) state_slice = dae_map.pop(state[index0]) if state[index0].fixed: ic_vars.append(state_slice) deriv_vars.append(deriv_slice) diff_vars.append(state_slice) else: # Neither is fixed. This should be the most common case. state_slice = dae_map.pop(state[index0]) if state[index0].fixed: ic_vars.append(state_slice) deriv_vars.append(deriv_slice) diff_vars.append(state_slice) if not updated_input_set: raise RuntimeError('Not all inputs could be found') assert len(deriv_vars) == len(diff_vars) for var0, time_slice in dae_map.items(): var1 = time_slice[t1] # If the variable is still in the list of time-indexed vars, # it must either be fixed (not a var) or be an algebraic var if var1.fixed: fixed_vars.append(time_slice) else: if var0.fixed: ic_vars.append(time_slice) alg_vars.append(time_slice) namespace.deriv_vars = NMPCVarGroup(deriv_vars, time) namespace.diff_vars = NMPCVarGroup(diff_vars, time) namespace.n_diff_vars = len(diff_vars) namespace.n_deriv_vars = len(deriv_vars) assert (namespace.n_diff_vars == namespace.n_deriv_vars) # ic_vars will not be stored as a NMPCVarGroup - don't want to store # all the info twice namespace.ic_vars = ic_vars namespace.n_ic_vars = len(ic_vars) #assert model.n_dv == len(ic_vars) # Would like this to be true, but accurately detecting differential # variables that are not implicitly fixed (by fixing some input) # is difficult # Also, a categorization can have no input vars and still be # valid for MHE namespace.input_vars = NMPCVarGroup(input_vars, time) namespace.n_input_vars = len(input_vars) namespace.alg_vars = NMPCVarGroup(alg_vars, time) namespace.n_alg_vars = len(alg_vars) namespace.fixed_vars = NMPCVarGroup(fixed_vars, time) namespace.n_fixed_vars = len(fixed_vars) namespace.variables_categorized = True