def _map_variable_stages(model): variable_stage_annotation = locate_annotations(model, VariableStageAnnotation, max_allowed=1) if len(variable_stage_annotation) == 0: raise ValueError("Reference model is missing variable stage " "annotation: %s" % (VariableStageAnnotation.__name__)) else: assert len(variable_stage_annotation) == 1 variable_stage_annotation = variable_stage_annotation[0][1] variable_stage_assignments = ComponentMap( variable_stage_annotation.expand_entries()) if len(variable_stage_assignments) == 0: raise ValueError("At least one variable stage assignment " "is required.") min_stagenumber = min(variable_stage_assignments.values(), key=lambda x: x[0])[0] max_stagenumber = max(variable_stage_assignments.values(), key=lambda x: x[0])[0] if max_stagenumber > 2: for var, (stagenum, derived) in \ variable_stage_assignments.items(): if stagenum > 2: raise ValueError( "Embedded stochastic programs must be two-stage " "(for now), but variable with name '%s' has been " "annotated with stage number: %s" % (var.name, stagenum)) stage_to_variables_map = {} stage_to_variables_map[1] = [] stage_to_variables_map[2] = [] for var in model.component_data_objects( Var, active=True, descend_into=True, sort=SortComponents.alphabetizeComponentAndIndex): stagenumber, derived = \ variable_stage_assignments.get(var, (2, False)) if (stagenumber != 1) and (stagenumber != 2): raise ValueError("Invalid stage annotation for variable with " "name '%s'. Stage assignment must be 1 or 2. " "Current value: %s" % (var.name, stagenumber)) if (stagenumber == 1): stage_to_variables_map[1].append((var, derived)) else: assert stagenumber == 2 stage_to_variables_map[2].append((var, derived)) variable_to_stage_map = ComponentMap() for stagenum, stagevars in stage_to_variables_map.items(): for var, derived in stagevars: variable_to_stage_map[var] = (stagenum, derived) return (stage_to_variables_map, variable_to_stage_map, variable_stage_assignments)
def test_perfect_matching(self): model = make_gas_expansion_model() # These are the variables and constraints of the square, # nonsingular subsystem variables = [] variables.extend(model.P.values()) variables.extend(model.T[i] for i in model.streams if i != model.streams.first()) variables.extend(model.rho[i] for i in model.streams if i != model.streams.first()) variables.extend(model.F[i] for i in model.streams if i != model.streams.first()) constraints = list(model.component_data_objects(pyo.Constraint)) imat = get_structural_incidence_matrix(variables, constraints) con_idx_map = ComponentMap((c, i) for i, c in enumerate(constraints)) n_var = len(variables) matching = maximum_matching(imat) matching = ComponentMap( (c, variables[matching[con_idx_map[c]]]) for c in constraints) values = ComponentSet(matching.values()) self.assertEqual(len(matching), n_var) self.assertEqual(len(values), n_var) # The subset of variables and equations we have identified # do not have a unique perfect matching. But we at least know # this much. self.assertIs(matching[model.ideal_gas[0]], model.P[0])
def test_triangularize(self): N = 5 model = make_gas_expansion_model(N) # These are the variables and constraints of the square, # nonsingular subsystem variables = [] variables.extend(model.P.values()) variables.extend(model.T[i] for i in model.streams if i != model.streams.first()) variables.extend(model.rho[i] for i in model.streams if i != model.streams.first()) variables.extend(model.F[i] for i in model.streams if i != model.streams.first()) constraints = list(model.component_data_objects(pyo.Constraint)) imat = get_structural_incidence_matrix(variables, constraints) con_idx_map = ComponentMap((c, i) for i, c in enumerate(constraints)) var_idx_map = ComponentMap((v, i) for i, v in enumerate(variables)) row_block_map, col_block_map = block_triangularize(imat) var_block_map = ComponentMap( (v, col_block_map[var_idx_map[v]]) for v in variables) con_block_map = ComponentMap( (c, row_block_map[con_idx_map[c]]) for c in constraints) var_values = set(var_block_map.values()) con_values = set(con_block_map.values()) self.assertEqual(len(var_values), N + 1) self.assertEqual(len(con_values), N + 1) self.assertEqual(var_block_map[model.P[0]], 0) for i in model.streams: if i != model.streams.first(): self.assertEqual(var_block_map[model.rho[i]], i) self.assertEqual(var_block_map[model.T[i]], i) self.assertEqual(var_block_map[model.P[i]], i) self.assertEqual(var_block_map[model.F[i]], i) self.assertEqual(con_block_map[model.ideal_gas[i]], i) self.assertEqual(con_block_map[model.expansion[i]], i) self.assertEqual(con_block_map[model.mbal[i]], i) self.assertEqual(con_block_map[model.ebal[i]], i)
def solve_separation_problem(model_data, config): # Timing variables global_solve_time = 0 local_solve_time = 0 # List of objective functions objectives_map = model_data.separation_model.util.map_obj_to_constr constraint_map_to_master = model_data.separation_model.util.map_new_constraint_list_to_original_con # Add additional or remaining separation objectives to the dict # (those either not assigned an explicit priority or those added by Pyros for ssv bounds) config_sep_priority_dict = config.separation_priority_order actual_sep_priority_dict = ComponentMap() for perf_con in model_data.separation_model.util.performance_constraints: actual_sep_priority_dict[perf_con] = config_sep_priority_dict.get( perf_con.name, 0) # "Bin" the objectives based on priorities sorted_unique_priorities = sorted(list( set(actual_sep_priority_dict.values())), reverse=True) set_of_deterministic_constraints = model_data.separation_model.util.deterministic_constraints if hasattr(model_data.separation_model, "epigraph_constr"): set_of_deterministic_constraints.add( model_data.separation_model.epigraph_constr) for is_global in (False, True): solver = config.global_solver if \ (is_global or config.bypass_local_separation) else config.local_solver solve_data_list = [] for val in sorted_unique_priorities: # Descending ordered by value # The list of performance constraints with this priority perf_constraints = [ constr_name for constr_name, priority in actual_sep_priority_dict.items() if priority == val ] for perf_con in perf_constraints: #config.progress_logger.info("Separating constraint " + str(perf_con)) try: separation_obj = objectives_map[perf_con] except: raise ValueError( "Error in mapping separation objective to its master constraint form." ) separation_obj.activate() if perf_con in set_of_deterministic_constraints: nom_constraint = perf_con else: nom_constraint = constraint_map_to_master[perf_con] try: model_data.master_nominal_scenario_value = value( model_data.master_nominal_scenario.find_component( nom_constraint)) except: raise ValueError( "Unable to access nominal scenario value for the constraint " + str(nom_constraint)) if config.uncertainty_set.geometry == Geometry.DISCRETE_SCENARIOS: solve_data_list.append( discrete_solve(model_data=model_data, config=config, solver=solver, is_global=is_global)) if all(s.termination_condition in globally_acceptable for sep_soln_list in solve_data_list for s in sep_soln_list) or \ (is_global == False and all(s.termination_condition in locally_acceptable for sep_soln_list in solve_data_list for s in sep_soln_list)): exit_separation_loop = False else: exit_separation_loop = True else: solve_data = SeparationResult() exit_separation_loop = solver_call_separation( model_data=model_data, config=config, solver=solver, solve_data=solve_data, is_global=is_global) solve_data_list.append([solve_data]) # === Keep track of total solve times if is_global or config.bypass_local_separation: if config.uncertainty_set.geometry == Geometry.DISCRETE_SCENARIOS: for sublist in solve_data_list: for s in sublist: global_solve_time += get_time_from_solver( s.results) else: global_solve_time += get_time_from_solver( solve_data.results) else: if config.uncertainty_set.geometry == Geometry.DISCRETE_SCENARIOS: for sublist in solve_data_list: for s in sublist: local_solve_time += get_time_from_solver( s.results) else: local_solve_time += get_time_from_solver( solve_data.results) # === Terminate for timing if exit_separation_loop: return solve_data_list, [], [], is_global, local_solve_time, global_solve_time separation_obj.deactivate() # Do we return? # If their are multiple violations in this bucket, pick the worst-case idx_i, idx_j = get_index_of_max_violation( model_data=model_data, config=config, solve_data_list=solve_data_list) violating_realizations = [ v for v in solve_data_list[idx_i][idx_j].violating_param_realization ] violations = solve_data_list[idx_i][idx_j].list_of_scaled_violations if any(s.found_violation for solve_list in solve_data_list for s in solve_list): #config.progress_logger.info( # "Violation found in constraint %s with realization %s" % ( # list(objectives_map.keys())[idx_i], violating_realizations)) return solve_data_list, violating_realizations, violations, is_global, local_solve_time, global_solve_time return solve_data_list, [], [], is_global, local_solve_time, global_solve_time
class GurobiDirect(DirectSolver): _verified_license = None def __init__(self, **kwds): if 'type' not in kwds: kwds['type'] = 'gurobi_direct' super(GurobiDirect, 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._needs_updated = True # flag that indicates if solver_model.update() needs called before getting variable and constraint attributes self._callback = None self._callback_func = None 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! logger.error("Import of gurobipy failed - gurobi message=%s\n" % (e, )) 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 available(self, exception_flag=True): if not super().available(exception_flag): return False if self._verified_license is None: try: # verify that we can get a Gurobi license m = self._gurobipy.Model() m.dispose() GurobiDirect._verified_license = True except Exception as e: logger.error("Could not create Model - gurobi message=%s\n" % (e, )) GurobiDirect._verified_license = False return self._verified_license 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._callback) self._needs_updated = False 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 _gurobi_lb_ub_from_var(self, var): if var.is_fixed(): val = var.value return val, val if var.has_lb(): lb = value(var.lb) else: lb = -self._gurobipy.GRB.INFINITY if var.has_ub(): ub = value(var.ub) else: ub = self._gurobipy.GRB.INFINITY return lb, ub def _add_var(self, var): varname = self._symbol_map.getSymbol(var, self._labeler) vtype = self._gurobi_vtype_from_var(var) lb, ub = self._gurobi_lb_ub_from_var(var) 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 self._needs_updated = True 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 Gurobi?\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) 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 self._needs_updated = True 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 self._needs_updated = True 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 self._needs_updated = True 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)) self._needs_updated = True 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) def _update(self): self._solver_model.update() self._needs_updated = False
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._pyomo_cone_to_solver_cone_map = dict() self._solver_cone_to_pyomo_cone_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_cone_data(self, con): # if the cone is not recognized, this function # will return None for cone_type and cone_members cone_type = None cone_param = 0 cone_members = None if isinstance(con, quadratic): assert con.has_ub() and \ (con.ub == 0) and (not con.has_lb()) assert con.check_convexity_conditions(relax=True) cone_type = self._mosek.conetype.quad cone_members = [con.r] + list(con.x) elif isinstance(con, rotated_quadratic): assert con.has_ub() and \ (con.ub == 0) and (not con.has_lb()) assert con.check_convexity_conditions(relax=True) cone_type = self._mosek.conetype.rquad cone_members = [con.r1, con.r2] + list(con.x) elif self._version >= (9, 0, 0): if isinstance(con, primal_exponential): assert con.has_ub() and \ (con.ub == 0) and (not con.has_lb()) assert con.check_convexity_conditions( relax=False) cone_type = self._mosek.conetype.pexp cone_members = [con.r, con.x1, con.x2] elif isinstance(con, primal_power): assert con.has_ub() and \ (con.ub == 0) and (not con.has_lb()) assert con.check_convexity_conditions( relax=False) cone_type = self._mosek.conetype.ppow cone_param = value(con.alpha) cone_members = [con.r1, con.r2] + list(con.x) elif isinstance(con, dual_exponential): assert con.has_ub() and \ (con.ub == 0) and (not con.has_lb()) assert con.check_convexity_conditions( relax=False) cone_type = self._mosek.conetype.dexp cone_members = [con.r, con.x1, con.x2] elif isinstance(con, dual_power): assert con.has_ub() and \ (con.ub == 0) and (not con.has_lb()) assert con.check_convexity_conditions( relax=False) cone_type = self._mosek.conetype.dpow cone_param = value(con.alpha) cone_members = [con.r1, con.r2] + list(con.x) return cone_type, cone_param, cone_members 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_cone_to_solver_cone_map = dict() self._solver_cone_to_pyomo_cone_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) mosek_expr = None referenced_vars = None cone_type = None cone_param = 0 cone_members = None if con._linear_canonical_form: mosek_expr, referenced_vars = self._get_expr_from_pyomo_repn( con.canonical_form(), self._max_constraint_degree) elif isinstance(con, _ConicBase): cone_type, cone_param, cone_members = \ self._get_cone_data(con) if cone_type is not None: assert cone_members is not None referenced_vars = ComponentSet(cone_members) else: logger.warning("Cone %s was not recognized by Mosek" % (str(con))) # the cone was not recognized, treat # it like a standard constraint, which # will in all likelihood lead to Mosek # reporting a helpful error message assert mosek_expr is None if (mosek_expr is None) and (cone_type is None): mosek_expr, referenced_vars = \ self._get_expr_from_pyomo_expr( con.body, self._max_constraint_degree) assert referenced_vars is not None if mosek_expr is not None: assert cone_type is None 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) self._pyomo_con_to_solver_con_map[con] = con_index self._solver_con_to_pyomo_con_map[con_index] = con else: assert cone_type is not None members = [self._pyomo_var_to_solver_var_map[v_] for v_ in cone_members] self._solver_model.appendcone(cone_type, cone_param, members) cone_index = self._solver_model.getnumcone()-1 self._solver_model.putconename(cone_index, conname) self._pyomo_cone_to_solver_cone_map[con] = cone_index self._solver_cone_to_pyomo_cone_map[cone_index] = con for var in referenced_vars: self._referenced_variables[var] += 1 self._vars_referenced_by_con[con] = referenced_vars 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, msk.problemtype.conic] 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_infeas: '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] = {} """TODO wrong length, needs to be getnumvars() mosek_cones = list(range(msk_task.getnumcone())) cone_names = [] for cone in mosek_cones: cone_names.append(msk_task.getconename(cone)) for name in cone_names: soln_constraints[name] = {} """ if extract_duals: ncon = msk_task.getnumcon() if ncon > 0: vals = [0.0]*ncon msk_task.gety(whichsol, vals) for val, name in zip(vals, con_names): soln_constraints[name]["Dual"] = val """TODO: wrong length, needs to be getnumvars() ncone = msk_task.getnumcone() if ncone > 0: vals = [0.0]*ncone msk_task.getsnx(whichsol, vals) for val, name in zip(vals, cone_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, objs_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 cone_map = self._pyomo_cone_to_solver_cone_map reverse_cone_map = self._solver_cone_to_pyomo_cone_map dual = self._pyomo_model.dual if objs_to_load is None: # constraints mosek_cons_to_load = range(self._solver_model.getnumcon()) vals = [0.0]*len(mosek_cons_to_load) 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 """TODO wrong length, needs to be getnumvars() # cones mosek_cones_to_load = range(self._solver_model.getnumcone()) vals = [0.0]*len(mosek_cones_to_load) self._solver_model.getsnx(self._whichsol, vals) for mosek_cone, val in zip(mosek_cones_to_load, vals): pyomo_cone = reverse_cone_map[mosek_cone] dual[pyomo_cone] = val """ else: mosek_cons_to_load = [] mosek_cones_to_load = [] for obj in objs_to_load: if obj in con_map: mosek_cons_to_load.append(con_map[obj]) else: # assume it is a cone mosek_cones_to_load.append(cone_map[obj]) # constraints mosek_cons_first = min(mosek_cons_to_load) mosek_cons_last = max(mosek_cons_to_load) vals = [0.0]*(mosek_cons_last - mosek_cons_first + 1) self._solver_model.getyslice(self._whichsol, mosek_cons_first, mosek_cons_last, vals) for mosek_con in mosek_cons_to_load: slice_index = mosek_con - mosek_cons_first val = vals[slice_index] pyomo_con = reverse_con_map[mosek_con] dual[pyomo_con] = val """TODO wrong length, needs to be getnumvars() # cones mosek_cones_first = min(mosek_cones_to_load) mosek_cones_last = max(mosek_cones_to_load) vals = [0.0]*(mosek_cones_last - mosek_cones_first + 1) self._solver_model.getsnxslice(self._whichsol, mosek_cones_first, mosek_cones_last, vals) for mosek_cone in mosek_cones_to_load: slice_index = mosek_cone - mosek_cones_first val = vals[slice_index] pyomo_cone = reverse_cone_map[mosek_cone] dual[pyomo_cone] = 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 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_components(model, time, Var) 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
def test_values(self): cmap = ComponentMap(self._components) self.assertEqual(sorted(cmap.values()), sorted(list(val for c,val in self._components)))