def process_soln_file(self, results): # the only suffixes that we extract from CBC are # constraint duals 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_reduced_costs = False for suffix in self._suffixes: flag = False if re.match(suffix, "dual"): extract_duals = True flag = True if re.match(suffix, "rc"): extract_reduced_costs = True flag = True if not flag: raise RuntimeError( "***CBC solver plugin cannot extract solution suffix=" + suffix) # if dealing with SOL format files, we've already read # this via the base class reader functionality. if self._results_format is ResultsFormat.sol: return # otherwise, go with the native CBC solution format. if len(results.solution) > 0: solution = results.solution(0) else: solution = Solution() results.problem.number_of_objectives = 1 processing_constraints = None # None means header, True means constraints, False means variables. header_processed = False optim_value = None try: INPUT = open(self._soln_file, "r") except IOError: INPUT = [] for line in INPUT: tokens = tuple(re.split('[ \t]+', line.strip())) n_tokens = len(tokens) # # These are the only header entries CBC will generate (identified via browsing CbcSolver.cpp) # See https://projects.coin-or.org/Cbc/browser/trunk/Cbc/src/CbcSolver.cpp # Search for (no integer solution - continuous used) Currently line 9912 as of rev2497 # Note that since this possibly also covers old CBC versions, we shall not be removing any functionality, # even if it is not seen in the current revision # if not header_processed: if tokens[0] == 'Optimal': results.solver.termination_condition = TerminationCondition.optimal results.solver.status = SolverStatus.ok results.solver.termination_message = "Model was solved to optimality (subject to tolerances), " \ "and an optimal solution is available." solution.status = SolutionStatus.optimal optim_value = float(tokens[-1]) elif tokens[0] in ('Infeasible', 'PrimalInfeasible') or ( n_tokens > 1 and tokens[0:2] == ('Integer', 'infeasible')): results.solver.termination_message = "Model was proven to be infeasible." results.solver.termination_condition = TerminationCondition.infeasible results.solver.status = SolverStatus.warning solution.status = SolutionStatus.infeasible INPUT.close() return elif tokens[0] == 'Unbounded' or ( n_tokens > 2 and tokens[0] == 'Problem' and tokens[2] == 'unbounded') or (n_tokens > 1 and tokens[0:2] == ('Dual', 'infeasible')): results.solver.termination_message = "Model was proven to be unbounded." results.solver.termination_condition = TerminationCondition.unbounded results.solver.status = SolverStatus.warning solution.status = SolutionStatus.unbounded INPUT.close() return elif n_tokens > 2 and tokens[0:2] == ('Stopped', 'on'): optim_value = float(tokens[-1]) solution.gap = None results.solver.status = SolverStatus.aborted solution.status = SolutionStatus.stoppedByLimit if tokens[2] == 'time': results.solver.termination_message = "Optimization terminated because the time expended " \ "exceeded the value specified in the seconds " \ "parameter." results.solver.termination_condition = TerminationCondition.maxTimeLimit elif tokens[2] == 'iterations': # Only add extra info if not already obtained from logs (which give a better description) if results.solver.termination_condition not in [ TerminationCondition.maxEvaluations, TerminationCondition.other, TerminationCondition.maxIterations ]: results.solver.termination_message = "Optimization terminated because a limit was hit" results.solver.termination_condition = TerminationCondition.maxIterations elif tokens[2] == 'difficulties': results.solver.termination_condition = TerminationCondition.solverFailure results.solver.status = SolverStatus.error solution.status = SolutionStatus.error elif tokens[2] == 'ctrl-c': results.solver.termination_message = "Optimization was terminated by the user." results.solver.termination_condition = TerminationCondition.userInterrupt solution.status = SolutionStatus.unknown else: results.solver.termination_condition = TerminationCondition.unknown results.solver.status = SolverStatus.unknown solution.status = SolutionStatus.unknown results.solver.termination_message = ' '.join(tokens) print( '***WARNING: CBC plugin currently not processing solution status=Stopped correctly. Full ' 'status line is: {}'.format(line.strip())) if n_tokens > 8 and tokens[3:9] == ('(no', 'integer', 'solution', '-', 'continuous', 'used)'): results.solver.termination_message = "Optimization terminated because a limit was hit, " \ "however it had not found an integer solution yet." results.solver.termination_condition = TerminationCondition.intermediateNonInteger solution.status = SolutionStatus.other else: results.solver.termination_condition = TerminationCondition.unknown results.solver.status = SolverStatus.unknown solution.status = SolutionStatus.unknown results.solver.termination_message = ' '.join(tokens) print( '***WARNING: CBC plugin currently not processing solution status={} correctly. Full status ' 'line is: {}'.format(tokens[0], line.strip())) # most of the first tokens should be integers # if it's not an integer, only then check the list of results try: row_number = int(tokens[0]) if row_number == 0: # indicates section start. if processing_constraints is None: processing_constraints = True elif processing_constraints is True: processing_constraints = False else: raise RuntimeError( "CBC plugin encountered unexpected line=(" + line.strip() + ") in solution file=" + self._soln_file + "; constraint and variable sections already processed!" ) except ValueError: if tokens[0] in ("Optimal", "Infeasible", "Unbounded", "Stopped", "Integer", "Status"): if optim_value is not None: if results.problem.sense == ProblemSense.maximize: optim_value *= -1 solution.objective['__default_objective__'] = { 'Value': optim_value } header_processed = True if (processing_constraints is True) and (extract_duals is True): if n_tokens == 4: pass elif (n_tokens == 5) and tokens[0] == "**": tokens = tokens[1:] else: raise RuntimeError( "Unexpected line format encountered in CBC solution file - line=" + line) constraint = tokens[1] constraint_ax = float( tokens[2] ) # CBC reports the constraint row times the solution vector - not the slack. constraint_dual = float(tokens[3]) if constraint[:2] == 'c_': solution.constraint[constraint] = {"Dual": constraint_dual} elif constraint[:2] == 'r_': # For the range constraints, supply only the dual with the largest # magnitude (at least one should always be numerically zero) existing_constraint_dual_dict = solution.constraint.get( 'r_l_' + constraint[4:], None) if existing_constraint_dual_dict: # if a constraint dual is already saved, then update it if its # magnitude is larger than existing; this avoids checking vs # zero (avoiding problems with solver tolerances) existing_constraint_dual = existing_constraint_dual_dict[ "Dual"] if abs(constraint_dual) > abs( existing_constraint_dual): solution.constraint['r_l_' + constraint[4:]] = { "Dual": constraint_dual } else: # if no constraint with that name yet, just save it in the solution constraint dictionary solution.constraint['r_l_' + constraint[4:]] = { "Dual": constraint_dual } elif processing_constraints is False: if n_tokens == 4: pass elif (n_tokens == 5) and tokens[0] == "**": tokens = tokens[1:] else: raise RuntimeError("Unexpected line format encountered " "in CBC solution file - line=" + line) variable_name = tokens[1] variable_value = float(tokens[2]) variable = solution.variable[variable_name] = { "Value": variable_value } if extract_reduced_costs is True: variable_reduced_cost = float( tokens[3]) # currently ignored. variable["Rc"] = variable_reduced_cost elif header_processed is True: pass else: raise RuntimeError("CBC plugin encountered unexpected " "line=(" + line.strip() + ") in solution file=" + self._soln_file + "; expecting header, but " "found data!") if not type(INPUT) is list: INPUT.close() if len(results.solution) == 0 and solution.status in [ SolutionStatus.optimal, SolutionStatus.stoppedByLimit, SolutionStatus.unknown, SolutionStatus.other ]: results.solution.insert(solution)
def process_soln_file(self, results): # the only suffixes that we extract from CPLEX 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 extract_rc = False extract_lrc = False extract_urc = 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 extract_rc = True flag = True if re.match(suffix, "lrc"): extract_reduced_costs = True extract_lrc = True flag = True if re.match(suffix, "urc"): extract_reduced_costs = True extract_urc = True flag = True if not flag: raise RuntimeError( "***The CPLEX solver plugin cannot extract solution suffix=" + suffix) # check for existence of the solution file # not sure why we just return - would think that we # would want to indicate some sort of error if not os.path.exists(self._soln_file): return range_duals = {} range_slacks = {} soln = Solution() soln.objective['__default_objective__'] = {'Value': None} # caching for efficiency soln_variables = soln.variable soln_constraints = soln.constraint INPUT = open(self._soln_file, "r") results.problem.number_of_objectives = 1 time_limit_exceeded = False mip_problem = False for line in INPUT: line = line.strip() line = line.lstrip('<?/') line = line.rstrip('/>?') tokens = line.split(' ') if tokens[0] == "variable": variable_name = None variable_value = None variable_reduced_cost = None variable_status = None for i in xrange(1, len(tokens)): field_name = tokens[i].split('=')[0] field_value = tokens[i].split('=')[1].lstrip("\"").rstrip( "\"") if field_name == "name": variable_name = field_value elif field_name == "value": variable_value = field_value elif (extract_reduced_costs is True) and (field_name == "reducedCost"): variable_reduced_cost = field_value elif (extract_reduced_costs is True) and (field_name == "status"): variable_status = field_value # skip the "constant-one" variable, used to capture/retain objective offsets in the CPLEX LP format. if variable_name != "ONE_VAR_CONSTANT": variable = soln_variables[variable_name] = { "Value": float(variable_value) } if (variable_reduced_cost is not None) and (extract_reduced_costs is True): try: if extract_rc is True: variable["Rc"] = float(variable_reduced_cost) if variable_status is not None: if extract_lrc is True: if variable_status == "LL": variable["Lrc"] = float( variable_reduced_cost) else: variable["Lrc"] = 0.0 if extract_urc is True: if variable_status == "UL": variable["Urc"] = float( variable_reduced_cost) else: variable["Urc"] = 0.0 except: raise ValueError("Unexpected reduced-cost value=" + str(variable_reduced_cost) + " encountered for variable=" + variable_name) elif (tokens[0] == "constraint") and ((extract_duals is True) or (extract_slacks is True)): is_range = False rlabel = None rkey = None for i in xrange(1, len(tokens)): field_name = tokens[i].split('=')[0] field_value = tokens[i].split('=')[1].lstrip("\"").rstrip( "\"") if field_name == "name": if field_value.startswith('c_'): constraint = soln_constraints[field_value] = {} elif field_value.startswith('r_l_'): is_range = True rlabel = field_value[4:] rkey = 0 elif field_value.startswith('r_u_'): is_range = True rlabel = field_value[4:] rkey = 1 elif (extract_duals is True) and (field_name == "dual"): # for LPs if is_range is False: constraint["Dual"] = float(field_value) else: range_duals.setdefault( rlabel, [0, 0])[rkey] = float(field_value) elif (extract_slacks is True) and (field_name == "slack"): # for MIPs if is_range is False: constraint["Slack"] = float(field_value) else: range_slacks.setdefault( rlabel, [0, 0])[rkey] = float(field_value) elif tokens[0].startswith("problemName"): filename = ( tokens[0].split('=')[1].strip()).lstrip("\"").rstrip("\"") results.problem.name = os.path.basename(filename) if '.' in results.problem.name: results.problem.name = results.problem.name.split('.')[0] tINPUT = open(filename, "r") for tline in tINPUT: tline = tline.strip() if tline == "": continue tokens = re.split('[\t ]+', tline) if tokens[0][0] in ['\\', '*']: continue elif tokens[0] == "NAME": results.problem.name = tokens[1] else: sense = tokens[0].lower() if sense in ['max', 'maximize']: results.problem.sense = ProblemSense.maximize if sense in ['min', 'minimize']: results.problem.sense = ProblemSense.minimize break tINPUT.close() elif tokens[0].startswith("objectiveValue"): objective_value = ( tokens[0].split('=')[1].strip()).lstrip("\"").rstrip("\"") soln.objective['__default_objective__']['Value'] = float( objective_value) elif tokens[0].startswith("solutionStatusValue"): pieces = tokens[0].split("=") solution_status = eval(pieces[1]) # solution status = 1 => optimal # solution status = 3 => infeasible if soln.status == SolutionStatus.unknown: if solution_status == 1: soln.status = SolutionStatus.optimal elif solution_status == 3: soln.status = SolutionStatus.infeasible soln.gap = None else: # we are flagging anything with a solution status >= 4 as an error, to possibly # be over-ridden as we learn more about the status (e.g., due to time limit exceeded). soln.status = SolutionStatus.error soln.gap = None elif tokens[0].startswith("solutionStatusString"): solution_status = ((" ".join(tokens).split('=')[1]).strip() ).lstrip("\"").rstrip("\"") if solution_status in [ "optimal", "integer optimal solution", "integer optimal, tolerance" ]: soln.status = SolutionStatus.optimal soln.gap = 0.0 results.problem.lower_bound = soln.objective[ '__default_objective__']['Value'] results.problem.upper_bound = soln.objective[ '__default_objective__']['Value'] if "integer" in solution_status: mip_problem = True elif solution_status in ["infeasible"]: soln.status = SolutionStatus.infeasible soln.gap = None elif solution_status in ["time limit exceeded"]: # we need to know if the solution is primal feasible, and if it is, set the solution status accordingly. # for now, just set the flag so we can trigger the logic when we see the primalFeasible keyword. time_limit_exceeded = True elif tokens[0].startswith("MIPNodes"): if mip_problem: n = eval( eval((" ".join(tokens).split('=')[1] ).strip()).lstrip("\"").rstrip("\"")) results.solver.statistics.branch_and_bound.number_of_created_subproblems = n results.solver.statistics.branch_and_bound.number_of_bounded_subproblems = n elif tokens[0].startswith("primalFeasible") and ( time_limit_exceeded is True): primal_feasible = int(((" ".join(tokens).split('=')[1] ).strip()).lstrip("\"").rstrip("\"")) if primal_feasible == 1: soln.status = SolutionStatus.feasible if (results.problem.sense == ProblemSense.minimize): results.problem.upper_bound = soln.objective[ '__default_objective__']['Value'] else: results.problem.lower_bound = soln.objective[ '__default_objective__']['Value'] else: soln.status = SolutionStatus.infeasible if self._best_bound is not None: if results.problem.sense == ProblemSense.minimize: results.problem.lower_bound = self._best_bound else: results.problem.upper_bound = self._best_bound if self._gap is not None: soln.gap = self._gap # For the range constraints, supply only the dual with the largest # magnitude (at least one should always be numerically zero) for key, (ld, ud) in iteritems(range_duals): if abs(ld) > abs(ud): soln_constraints['r_l_' + key] = {"Dual": ld} else: soln_constraints['r_l_' + key] = { "Dual": ud } # Use the same key # slacks for key, (ls, us) in iteritems(range_slacks): if abs(ls) > abs(us): soln_constraints.setdefault('r_l_' + key, {})["Slack"] = ls else: soln_constraints.setdefault( 'r_l_' + key, {})["Slack"] = us # Use the same key if not results.solver.status is SolverStatus.error: if results.solver.termination_condition in [ TerminationCondition.unknown, #TerminationCondition.maxIterations, #TerminationCondition.minFunctionValue, #TerminationCondition.minStepLength, TerminationCondition.globallyOptimal, TerminationCondition.locallyOptimal, TerminationCondition.optimal, #TerminationCondition.maxEvaluations, TerminationCondition.other ]: results.solution.insert(soln) elif (results.solver.termination_condition is \ TerminationCondition.maxTimeLimit) and \ (soln.status is not SolutionStatus.infeasible): results.solution.insert(soln) INPUT.close()
def process_logfile(self): """ Process logfile """ results = SolverResults() # The logfile output for cbc when using nl files # provides no information worth parsing here if self._problem_format is ProblemFormat.nl: return results # # Initial values # soln = Solution() # # Process logfile # OUTPUT = open(self._log_file) output = "".join(OUTPUT.readlines()) OUTPUT.close() # # Parse logfile lines # results.problem.sense = ProblemSense.minimize results.problem.name = None optim_value = float('inf') lower_bound = None upper_bound = None gap = None nodes = None # See https://www.coin-or.org/Cbc/cbcuserguide.html#messages for line in output.split("\n"): tokens = tuple(re.split('[ \t]+', line.strip())) n_tokens = len(tokens) if n_tokens > 1: # https://projects.coin-or.org/Cbc/browser/trunk/Cbc/src/CbcSolver.cpp?rev=2497#L3769 if n_tokens > 4 and tokens[:4] == ('Continuous', 'objective', 'value', 'is'): lower_bound = float(tokens[4]) # Search completed - best objective %g, took %d iterations and %d nodes elif n_tokens > 12 and tokens[1:3] == ('Search', 'completed') \ and tokens[4:6] == ('best', 'objective') and tokens[9] == 'iterations' \ and tokens[12] == 'nodes': optim_value = float(tokens[6][:-1]) results.solver.statistics.black_box.number_of_iterations = int( tokens[8]) nodes = int(tokens[11]) elif tokens[1] == 'Exiting' and n_tokens > 4: if tokens[2:4] == ('on', 'maximum'): results.solver.termination_condition = { 'nodes': TerminationCondition.maxEvaluations, 'time': TerminationCondition.maxTimeLimit, 'solutions': TerminationCondition.other, 'iterations': TerminationCondition.maxIterations }.get(tokens[4], TerminationCondition.other) # elif tokens[2:5] == ('as', 'integer', 'gap'): # # We might want to handle this case # Integer solution of %g found... elif n_tokens >= 4 and tokens[1:4] == ('Integer', 'solution', 'of'): optim_value = float(tokens[4]) try: results.solver.statistics.black_box.number_of_iterations = \ int(tokens[tokens.index('iterations') - 1]) nodes = int(tokens[tokens.index('nodes') - 1]) except ValueError: pass # Partial search - best objective %g (best possible %g), took %d iterations and %d nodes elif n_tokens > 15 and tokens[1:3] == ('Partial', 'search') \ and tokens[4:6] == ('best', 'objective') and tokens[7:9] == ('(best', 'possible') \ and tokens[12] == 'iterations' and tokens[15] == 'nodes': optim_value = float(tokens[6]) lower_bound = float(tokens[9][:-2]) results.solver.statistics.black_box.number_of_iterations = int( tokens[11]) nodes = int(tokens[14]) elif n_tokens > 12 and tokens[1] == 'After' and tokens[3] == 'nodes,' \ and tokens[8:10] == ('best', 'solution,') and tokens[10:12] == ('best', 'possible'): nodes = int(tokens[2]) optim_value = float(tokens[7]) lower_bound = float(tokens[12]) elif tokens[0] == "Current" and n_tokens == 10 and tokens[1] == "default" and tokens[2] == "(if" \ and results.problem.name is None: results.problem.name = tokens[-1] if '.' in results.problem.name: parts = results.problem.name.split('.') if len(parts) > 2: results.problem.name = '.'.join(parts[:-1]) else: results.problem.name = results.problem.name.split( '.')[0] if '/' in results.problem.name: results.problem.name = results.problem.name.split( '/')[-1] if '\\' in results.problem.name: results.problem.name = results.problem.name.split( '\\')[-1] # https://projects.coin-or.org/Cbc/browser/trunk/Cbc/src/CbcSolver.cpp?rev=2497#L10840 elif tokens[0] == 'Presolve': if n_tokens > 9 and tokens[3] == 'rows,' and tokens[ 6] == 'columns': results.problem.number_of_variables = int( tokens[4]) - int(tokens[5][1:-1]) results.problem.number_of_constraints = int( tokens[1]) - int(tokens[2][1:-1]) results.problem.number_of_objectives = 1 elif n_tokens > 6 and tokens[6] == 'infeasible': soln.status = SolutionStatus.infeasible # https://projects.coin-or.org/Cbc/browser/trunk/Cbc/src/CbcSolver.cpp?rev=2497#L11105 elif n_tokens > 11 and tokens[:2] == ('Problem', 'has') and tokens[3] == 'rows,' and \ tokens[5] == 'columns' and tokens[7:9] == ('with', 'objective)'): results.problem.number_of_variables = int(tokens[4]) results.problem.number_of_constraints = int(tokens[2]) results.problem.number_of_nonzeros = int(tokens[6][1:]) results.problem.number_of_objectives = 1 # https://projects.coin-or.org/Cbc/browser/trunk/Cbc/src/CbcSolver.cpp?rev=2497#L10814 elif n_tokens > 8 and tokens[:3] == ('Original', 'problem', 'has') and tokens[4] == 'integers' \ and tokens[6:9] == ('of', 'which', 'binary)'): results.problem.number_of_integer_variables = int( tokens[3]) results.problem.number_of_binary_variables = int( tokens[5][1:]) elif n_tokens == 5 and tokens[3] == "NAME": results.problem.name = tokens[4] elif 'CoinLpIO::readLp(): Maximization problem reformulated as minimization' in ' '.join( tokens): results.problem.sense = ProblemSense.maximize # https://projects.coin-or.org/Cbc/browser/trunk/Cbc/src/CbcSolver.cpp?rev=2497#L3047 elif n_tokens > 3 and tokens[:2] == ('Result', '-'): if tokens[2:4] in [('Run', 'abandoned'), ('User', 'ctrl-c')]: results.solver.termination_condition = TerminationCondition.userInterrupt if n_tokens > 4: if tokens[2:5] == ('Optimal', 'solution', 'found'): # parser for log file generetated with discrete variable soln.status = SolutionStatus.optimal # if n_tokens > 7 and tokens[5:8] == ('(within', 'gap', 'tolerance)'): # # We might want to handle this case elif tokens[2:5] in [ ('Linear', 'relaxation', 'infeasible'), ('Problem', 'proven', 'infeasible') ]: soln.status = SolutionStatus.infeasible elif tokens[2:5] == ('Linear', 'relaxation', 'unbounded'): soln.status = SolutionStatus.unbounded elif n_tokens > 5 and tokens[2:4] == ( 'Stopped', 'on') and tokens[5] == 'limit': results.solver.termination_condition = { 'node': TerminationCondition.maxEvaluations, 'time': TerminationCondition.maxTimeLimit, 'solution': TerminationCondition.other, 'iterations': TerminationCondition.maxIterations }.get(tokens[4], TerminationCondition.other) # perhaps from https://projects.coin-or.org/Cbc/browser/trunk/Cbc/src/CbcSolver.cpp?rev=2497#L12318 elif n_tokens > 3 and tokens[2] == "Finished": soln.status = SolutionStatus.optimal optim_value = float(tokens[4]) # https://projects.coin-or.org/Cbc/browser/trunk/Cbc/src/CbcSolver.cpp?rev=2497#L7904 elif n_tokens >= 3 and tokens[:2] == ('Objective', 'value:'): # parser for log file generetated with discrete variable optim_value = float(tokens[2]) # https://projects.coin-or.org/Cbc/browser/trunk/Cbc/src/CbcSolver.cpp?rev=2497#L7904 elif n_tokens >= 4 and tokens[:4] == ('No', 'feasible', 'solution', 'found'): soln.status = SolutionStatus.infeasible elif n_tokens > 2 and tokens[:2] == ('Lower', 'bound:'): if lower_bound is None: # Only use if not already found since this is to less decimal places results.problem.lower_bound = float(tokens[2]) # https://projects.coin-or.org/Cbc/browser/trunk/Cbc/src/CbcSolver.cpp?rev=2497#L7918 elif tokens[0] == 'Gap:': # This is relative and only to 2 decimal places - could calculate explicitly using lower bound gap = float(tokens[1]) # https://projects.coin-or.org/Cbc/browser/trunk/Cbc/src/CbcSolver.cpp?rev=2497#L7923 elif n_tokens > 2 and tokens[:2] == ('Enumerated', 'nodes:'): nodes = int(tokens[2]) # https://projects.coin-or.org/Cbc/browser/trunk/Cbc/src/CbcSolver.cpp?rev=2497#L7926 elif n_tokens > 2 and tokens[:2] == ('Total', 'iterations:'): results.solver.statistics.black_box.number_of_iterations = int( tokens[2]) # https://projects.coin-or.org/Cbc/browser/trunk/Cbc/src/CbcSolver.cpp?rev=2497#L7930 elif n_tokens > 3 and tokens[:3] == ('Time', '(CPU', 'seconds):'): results.solver.system_time = float(tokens[3]) # https://projects.coin-or.org/Cbc/browser/trunk/Cbc/src/CbcSolver.cpp?rev=2497#L7933 elif n_tokens > 3 and tokens[:3] == ('Time', '(Wallclock', 'Seconds):'): results.solver.wallclock_time = float(tokens[3]) # https://projects.coin-or.org/Cbc/browser/trunk/Cbc/src/CbcSolver.cpp?rev=2497#L10477 elif n_tokens > 4 and tokens[:4] == ('Total', 'time', '(CPU', 'seconds):'): results.solver.system_time = float(tokens[4]) if n_tokens > 7 and tokens[5:7] == ('(Wallclock', 'seconds):'): results.solver.wallclock_time = float(tokens[7]) elif tokens[0] == "Optimal": if n_tokens > 4 and tokens[ 2] == "objective" and tokens[4] != "and": # parser for log file generetated without discrete variable # see pull request #339: last check avoids lines like "Optimal - objective gap and # complementarity gap both smallish and small steps" soln.status = SolutionStatus.optimal optim_value = float(tokens[4]) elif n_tokens > 5 and tokens[1] == 'objective' and tokens[ 5] == 'iterations': soln.status = SolutionStatus.optimal optim_value = float(tokens[2]) results.solver.statistics.black_box.number_of_iterations = int( tokens[4]) elif tokens[0] == "sys" and n_tokens == 2: results.solver.system_time = float(tokens[1]) elif tokens[0] == "user" and n_tokens == 2: results.solver.user_time = float(tokens[1]) elif n_tokens == 10 and "Presolve" in tokens and \ "iterations" in tokens and tokens[0] == "Optimal" and "objective" == tokens[1]: soln.status = SolutionStatus.optimal optim_value = float(tokens[2]) results.solver.user_time = -1.0 # Why is this set to -1? if results.problem.name is None: results.problem.name = 'unknown' if soln.status is SolutionStatus.optimal: results.solver.termination_message = "Model was solved to optimality (subject to tolerances), and an " \ "optimal solution is available." results.solver.termination_condition = TerminationCondition.optimal results.solver.status = SolverStatus.ok if gap is None: lower_bound = optim_value gap = 0.0 elif soln.status == SolutionStatus.infeasible: results.solver.termination_message = "Model was proven to be infeasible." results.solver.termination_condition = TerminationCondition.infeasible results.solver.status = SolverStatus.warning elif soln.status == SolutionStatus.unbounded: results.solver.termination_message = "Model was proven to be unbounded." results.solver.termination_condition = TerminationCondition.unbounded results.solver.status = SolverStatus.warning elif results.solver.termination_condition in [ TerminationCondition.maxTimeLimit, TerminationCondition.maxEvaluations, TerminationCondition.other, TerminationCondition.maxIterations ]: results.solver.status = SolverStatus.aborted soln.status = SolutionStatus.stoppedByLimit if results.solver.termination_condition == TerminationCondition.maxTimeLimit: results.solver.termination_message = "Optimization terminated because the time expended " \ "exceeded the value specified in the seconds " \ "parameter." elif results.solver.termination_condition == TerminationCondition.maxEvaluations: results.solver.termination_message = \ "Optimization terminated because the total number of branch-and-cut nodes explored " \ "exceeded the value specified in the maxNodes parameter" elif results.solver.termination_condition == TerminationCondition.other: results.solver.termination_message = "Optimization terminated because the number of " \ "solutions found reached the value specified in the " \ "maxSolutions parameter." elif results.solver.termination_condition == TerminationCondition.maxIterations: results.solver.termination_message = "Optimization terminated because the total number of simplex " \ "iterations performed exceeded the value specified in the " \ "maxIterations parameter." soln.gap = gap if results.problem.sense == ProblemSense.minimize: upper_bound = optim_value elif results.problem.sense == ProblemSense.maximize: optim_value *= -1 upper_bound = None if lower_bound is None else -lower_bound lower_bound = optim_value soln.objective['__default_objective__'] = {'Value': optim_value} results.problem.lower_bound = lower_bound results.problem.upper_bound = upper_bound results.solver.statistics.branch_and_bound.number_of_bounded_subproblems = nodes results.solver.statistics.branch_and_bound.number_of_created_subproblems = nodes if soln.status in [ SolutionStatus.optimal, SolutionStatus.stoppedByLimit, SolutionStatus.unknown, SolutionStatus.other ]: results.solution.insert(soln) return results
def solve(self, *args, **kwds): """ Solve a model via the GAMS Python API. Keyword Arguments ----------------- tee=False: bool Output GAMS log to stdout. logfile=None: str Filename to output GAMS log to a file. load_solutions=True: bool Load solution into model. If False, the results object will contain the solution data. keepfiles=False: bool Keep temporary files. Equivalent of DebugLevel.KeepFiles. Summary of temp files can be found in _gams_py_gjo0.pf tmpdir=None: str Specify directory path for storing temporary files. A directory will be created if one of this name doesn't exist. By default uses the system default temporary path. report_timing=False: bool Print timing reports for presolve, solver, postsolve, etc. io_options: dict Options that get passed to the writer. See writer in pyomo.repn.plugins.gams_writer for details. Updated with any other keywords passed to solve method. """ # Make sure available() doesn't crash self.available() from gams import GamsWorkspace, DebugLevel from gams.workspace import GamsExceptionExecution if len(args) != 1: raise ValueError('Exactly one model must be passed ' 'to solve method of GAMSSolver.') model = args[0] # self.options are default for each run, overwritten by kwds options = dict() options.update(self.options) options.update(kwds) load_solutions = options.pop("load_solutions", True) tee = options.pop("tee", False) logfile = options.pop("logfile", None) keepfiles = options.pop("keepfiles", False) tmpdir = options.pop("tmpdir", None) report_timing = options.pop("report_timing", False) io_options = options.pop("io_options", {}) # Pass remaining keywords to writer, which will handle # any unrecognized arguments io_options.update(options) initial_time = time.time() #################################################################### # Presolve #################################################################### # Create StringIO stream to pass to gams_writer, on which the # model file will be written. The writer also passes this StringIO # back, but output_file is defined in advance for clarity. output_file = StringIO() if isinstance(model, IBlock): # Kernel blocks have slightly different write method smap_id = model.write(filename=output_file, format=ProblemFormat.gams, _called_by_solver=True, **io_options) symbolMap = getattr(model, "._symbol_maps")[smap_id] else: (_, smap_id) = model.write(filename=output_file, format=ProblemFormat.gams, io_options=io_options) symbolMap = model.solutions.symbol_map[smap_id] presolve_completion_time = time.time() if report_timing: print(" %6.2f seconds required for presolve" % (presolve_completion_time - initial_time)) #################################################################### # Apply solver #################################################################### # IMPORTANT - only delete the whole tmpdir if the solver was the one # that made the directory. Otherwise, just delete the files the solver # made, if not keepfiles. That way the user can select a directory # they already have, like the current directory, without having to # worry about the rest of the contents of that directory being deleted. newdir = True if tmpdir is not None and os.path.exists(tmpdir): newdir = False ws = GamsWorkspace( debug=DebugLevel.KeepFiles if keepfiles else DebugLevel.Off, working_directory=tmpdir) t1 = ws.add_job_from_string(output_file.getvalue()) try: with OutputStream(tee=tee, logfile=logfile) as output_stream: t1.run(output=output_stream) except GamsExceptionExecution as e: try: if e.rc == 3: # Execution Error check_expr_evaluation(model, symbolMap, 'direct') finally: # Always name working directory or delete files, # regardless of any errors. if keepfiles: print("\nGAMS WORKING DIRECTORY: %s\n" % ws.working_directory) elif tmpdir is not None: # Garbage collect all references to t1.out_db # So that .gdx file can be deleted t1 = rec = rec_lo = rec_hi = None file_removal_gams_direct(tmpdir, newdir) raise except: # Catch other errors and remove files first if keepfiles: print("\nGAMS WORKING DIRECTORY: %s\n" % ws.working_directory) elif tmpdir is not None: # Garbage collect all references to t1.out_db # So that .gdx file can be deleted t1 = rec = rec_lo = rec_hi = None file_removal_gams_direct(tmpdir, newdir) raise solve_completion_time = time.time() if report_timing: print(" %6.2f seconds required for solver" % (solve_completion_time - presolve_completion_time)) #################################################################### # Postsolve #################################################################### # import suffixes must be on the top-level model if isinstance(model, IBlock): model_suffixes = list(comp.storage_key for comp \ in pyomo.core.kernel.suffix.\ import_suffix_generator(model, active=True, descend_into=False)) else: model_suffixes = list(name for (name,comp) \ in pyomo.core.base.suffix.\ active_import_suffix_generator(model)) extract_dual = ('dual' in model_suffixes) extract_rc = ('rc' in model_suffixes) results = SolverResults() results.problem.name = t1.name results.problem.lower_bound = t1.out_db["OBJEST"].find_record().value results.problem.upper_bound = t1.out_db["OBJEST"].find_record().value results.problem.number_of_variables = \ t1.out_db["NUMVAR"].find_record().value results.problem.number_of_constraints = \ t1.out_db["NUMEQU"].find_record().value results.problem.number_of_nonzeros = \ t1.out_db["NUMNZ"].find_record().value results.problem.number_of_binary_variables = None # Includes binary vars: results.problem.number_of_integer_variables = \ t1.out_db["NUMDVAR"].find_record().value results.problem.number_of_continuous_variables = \ t1.out_db["NUMVAR"].find_record().value \ - t1.out_db["NUMDVAR"].find_record().value results.problem.number_of_objectives = 1 # required by GAMS writer obj = list(model.component_data_objects(Objective, active=True)) assert len(obj) == 1, 'Only one objective is allowed.' obj = obj[0] objctvval = t1.out_db["OBJVAL"].find_record().value if obj.is_minimizing(): results.problem.sense = ProblemSense.minimize results.problem.upper_bound = objctvval else: results.problem.sense = ProblemSense.maximize results.problem.lower_bound = objctvval results.solver.name = "GAMS " + str(self.version()) # Init termination condition to None to give preference to this first # block of code, only set certain TC's below if it's still None results.solver.termination_condition = None results.solver.message = None solvestat = t1.out_db["SOLVESTAT"].find_record().value if solvestat == 1: results.solver.status = SolverStatus.ok elif solvestat == 2: results.solver.status = SolverStatus.ok results.solver.termination_condition = TerminationCondition.maxIterations elif solvestat == 3: results.solver.status = SolverStatus.ok results.solver.termination_condition = TerminationCondition.maxTimeLimit elif solvestat == 5: results.solver.status = SolverStatus.ok results.solver.termination_condition = TerminationCondition.maxEvaluations elif solvestat == 7: results.solver.status = SolverStatus.aborted results.solver.termination_condition = TerminationCondition.licensingProblems elif solvestat == 8: results.solver.status = SolverStatus.aborted results.solver.termination_condition = TerminationCondition.userInterrupt elif solvestat == 10: results.solver.status = SolverStatus.error results.solver.termination_condition = TerminationCondition.solverFailure elif solvestat == 11: results.solver.status = SolverStatus.error results.solver.termination_condition = TerminationCondition.internalSolverError elif solvestat == 4: results.solver.status = SolverStatus.warning results.solver.message = "Solver quit with a problem (see LST file)" elif solvestat in (9, 12, 13): results.solver.status = SolverStatus.error elif solvestat == 6: results.solver.status = SolverStatus.unknown results.solver.return_code = 0 # Not sure if this value is actually user time # "the elapsed time it took to execute a solve statement in total" results.solver.user_time = t1.out_db["ETSOLVE"].find_record().value results.solver.system_time = None results.solver.wallclock_time = None results.solver.termination_message = None soln = Solution() modelstat = t1.out_db["MODELSTAT"].find_record().value if modelstat == 1: results.solver.termination_condition = TerminationCondition.optimal soln.status = SolutionStatus.optimal elif modelstat == 2: results.solver.termination_condition = TerminationCondition.locallyOptimal soln.status = SolutionStatus.locallyOptimal elif modelstat in [3, 18]: results.solver.termination_condition = TerminationCondition.unbounded soln.status = SolutionStatus.unbounded elif modelstat in [4, 5, 6, 10, 19]: results.solver.termination_condition = TerminationCondition.infeasible soln.status = SolutionStatus.infeasible elif modelstat == 7: results.solver.termination_condition = TerminationCondition.feasible soln.status = SolutionStatus.feasible elif modelstat == 8: # 'Integer solution model found' results.solver.termination_condition = TerminationCondition.optimal soln.status = SolutionStatus.optimal elif modelstat == 9: results.solver.termination_condition = TerminationCondition.intermediateNonInteger soln.status = SolutionStatus.other elif modelstat == 11: # Should be handled above, if modelstat and solvestat both # indicate a licensing problem if results.solver.termination_condition is None: results.solver.termination_condition = TerminationCondition.licensingProblems soln.status = SolutionStatus.error elif modelstat in [12, 13]: if results.solver.termination_condition is None: results.solver.termination_condition = TerminationCondition.error soln.status = SolutionStatus.error elif modelstat == 14: if results.solver.termination_condition is None: results.solver.termination_condition = TerminationCondition.noSolution soln.status = SolutionStatus.unknown elif modelstat in [15, 16, 17]: # Having to do with CNS models, # not sure what to make of status descriptions results.solver.termination_condition = TerminationCondition.optimal soln.status = SolutionStatus.unsure else: # This is just a backup catch, all cases are handled above soln.status = SolutionStatus.error soln.gap = abs(results.problem.upper_bound \ - results.problem.lower_bound) for sym, ref in iteritems(symbolMap.bySymbol): obj = ref() if isinstance(model, IBlock): # Kernel variables have no 'parent_component' if obj.ctype is IObjective: soln.objective[sym] = {'Value': objctvval} if obj.ctype is not IVariable: continue else: if obj.parent_component().type() is Objective: soln.objective[sym] = {'Value': objctvval} if obj.parent_component().type() is not Var: continue rec = t1.out_db[sym].find_record() # obj.value = rec.level soln.variable[sym] = {"Value": rec.level} if extract_rc and not math.isnan(rec.marginal): # Do not set marginals to nan # model.rc[obj] = rec.marginal soln.variable[sym]['rc'] = rec.marginal if extract_dual: for c in model.component_data_objects(Constraint, active=True): if c.body.is_fixed() or \ (not (c.has_lb() or c.has_ub())): # the constraint was not sent to GAMS continue sym = symbolMap.getSymbol(c) if c.equality: rec = t1.out_db[sym].find_record() if not math.isnan(rec.marginal): # model.dual[c] = rec.marginal soln.constraint[sym] = {'dual': rec.marginal} else: # Solver didn't provide marginals, # nothing else to do here break else: # Inequality, assume if 2-sided that only # one side's marginal is nonzero # Negate marginal for _lo equations marg = 0 if c.lower is not None: rec_lo = t1.out_db[sym + '_lo'].find_record() marg -= rec_lo.marginal if c.upper is not None: rec_hi = t1.out_db[sym + '_hi'].find_record() marg += rec_hi.marginal if not math.isnan(marg): # model.dual[c] = marg soln.constraint[sym] = {'dual': marg} else: # Solver didn't provide marginals, # nothing else to do here break results.solution.insert(soln) if keepfiles: print("\nGAMS WORKING DIRECTORY: %s\n" % ws.working_directory) elif tmpdir is not None: # Garbage collect all references to t1.out_db # So that .gdx file can be deleted t1 = rec = rec_lo = rec_hi = None file_removal_gams_direct(tmpdir, newdir) #################################################################### # Finish with results #################################################################### results._smap_id = smap_id results._smap = None if isinstance(model, IBlock): if len(results.solution) == 1: results.solution(0).symbol_map = \ getattr(model, "._symbol_maps")[results._smap_id] results.solution(0).default_variable_value = \ self._default_variable_value if load_solutions: model.load_solution(results.solution(0)) else: assert len(results.solution) == 0 # see the hack in the write method # we don't want this to stick around on the model # after the solve assert len(getattr(model, "._symbol_maps")) == 1 delattr(model, "._symbol_maps") del results._smap_id if load_solutions and \ (len(results.solution) == 0): logger.error("No solution is available") else: if load_solutions: model.solutions.load_from(results) results._smap_id = None results.solution.clear() else: results._smap = model.solutions.symbol_map[smap_id] model.solutions.delete_symbol_map(smap_id) postsolve_completion_time = time.time() if report_timing: print(" %6.2f seconds required for postsolve" % (postsolve_completion_time - solve_completion_time)) print(" %6.2f seconds required total" % (postsolve_completion_time - initial_time)) return results
def solve(self, *args, **kwds): """ Solve a model via the GAMS executable. Keyword Arguments ----------------- tee=False: bool Output GAMS log to stdout. logfile=None: str Filename to output GAMS log to a file. load_solutions=True: bool Load solution into model. If False, the results object will contain the solution data. keepfiles=False: bool Keep temporary files. tmpdir=None: str Specify directory path for storing temporary files. A directory will be created if one of this name doesn't exist. By default uses the system default temporary path. report_timing=False: bool Print timing reports for presolve, solver, postsolve, etc. io_options: dict Options that get passed to the writer. See writer in pyomo.repn.plugins.gams_writer for details. Updated with any other keywords passed to solve method. Note: put_results is not available for modification on GAMSShell solver. """ # Make sure available() doesn't crash self.available() if len(args) != 1: raise ValueError('Exactly one model must be passed ' 'to solve method of GAMSSolver.') model = args[0] # self.options are default for each run, overwritten by kwds options = dict() options.update(self.options) options.update(kwds) load_solutions = options.pop("load_solutions", True) tee = options.pop("tee", False) logfile = options.pop("logfile", None) keepfiles = options.pop("keepfiles", False) tmpdir = options.pop("tmpdir", None) report_timing = options.pop("report_timing", False) io_options = options.pop("io_options", {}) io_options.update(options) # Pass remaining keywords to writer, which will handle # any unrecognized arguments initial_time = time.time() #################################################################### # Presolve #################################################################### # IMPORTANT - only delete the whole tmpdir if the solver was the one # that made the directory. Otherwise, just delete the files the solver # made, if not keepfiles. That way the user can select a directory # they already have, like the current directory, without having to # worry about the rest of the contents of that directory being deleted. newdir = False if tmpdir is None: tmpdir = mkdtemp() newdir = True elif not os.path.exists(tmpdir): # makedirs creates all necessary intermediate directories in order # to create the path to tmpdir, if they don't already exist. # However, if keepfiles is False, we only delete the final folder, # leaving the rest of the intermediate ones. os.makedirs(tmpdir) newdir = True output = "model.gms" output_filename = os.path.join(tmpdir, output) lst = "output.lst" lst_filename = os.path.join(tmpdir, lst) put_results = "results" io_options["put_results"] = put_results results_filename = os.path.join(tmpdir, put_results + ".dat") statresults_filename = os.path.join(tmpdir, put_results + "stat.dat") if isinstance(model, IBlock): # Kernel blocks have slightly different write method smap_id = model.write(filename=output_filename, format=ProblemFormat.gams, _called_by_solver=True, **io_options) symbolMap = getattr(model, "._symbol_maps")[smap_id] else: (_, smap_id) = model.write(filename=output_filename, format=ProblemFormat.gams, io_options=io_options) symbolMap = model.solutions.symbol_map[smap_id] presolve_completion_time = time.time() if report_timing: print(" %6.2f seconds required for presolve" % (presolve_completion_time - initial_time)) #################################################################### # Apply solver #################################################################### exe = self.executable() command = [exe, output, "o=" + lst, "curdir=" + tmpdir] if tee and not logfile: # default behaviour of gams is to print to console, for # compatability with windows and *nix we want to explicitly log to # stdout (see https://www.gams.com/latest/docs/UG_GamsCall.html) command.append("lo=3") elif not tee and not logfile: command.append("lo=0") elif not tee and logfile: command.append("lo=2") elif tee and logfile: command.append("lo=4") if logfile: command.append("lf=" + str(logfile)) try: rc, _ = pyutilib.subprocess.run(command, tee=tee) if keepfiles: print("\nGAMS WORKING DIRECTORY: %s\n" % tmpdir) if rc == 1 or rc == 127: raise RuntimeError("Command 'gams' was not recognized") elif rc != 0: if rc == 3: # Execution Error # Run check_expr_evaluation, which errors if necessary check_expr_evaluation(model, symbolMap, 'shell') # If nothing was raised, or for all other cases, raise this raise RuntimeError("GAMS encountered an error during solve. " "Check listing file for details.") with open(results_filename, 'r') as results_file: results_text = results_file.read() with open(statresults_filename, 'r') as statresults_file: statresults_text = statresults_file.read() finally: if not keepfiles: if newdir: shutil.rmtree(tmpdir) else: os.remove(output_filename) os.remove(lst_filename) os.remove(results_filename) os.remove(statresults_filename) solve_completion_time = time.time() if report_timing: print(" %6.2f seconds required for solver" % (solve_completion_time - presolve_completion_time)) #################################################################### # Postsolve #################################################################### # import suffixes must be on the top-level model if isinstance(model, IBlock): model_suffixes = list(comp.storage_key for comp \ in pyomo.core.kernel.suffix.\ import_suffix_generator(model, active=True, descend_into=False)) else: model_suffixes = list(name for (name,comp) \ in pyomo.core.base.suffix.\ active_import_suffix_generator(model)) extract_dual = ('dual' in model_suffixes) extract_rc = ('rc' in model_suffixes) stat_vars = dict() # Skip first line of explanatory text for line in statresults_text.splitlines()[1:]: items = line.split() try: stat_vars[items[0]] = float(items[1]) except ValueError: # GAMS printed NA, just make it nan stat_vars[items[0]] = float('nan') results = SolverResults() results.problem.name = output_filename results.problem.lower_bound = stat_vars["OBJEST"] results.problem.upper_bound = stat_vars["OBJEST"] results.problem.number_of_variables = stat_vars["NUMVAR"] results.problem.number_of_constraints = stat_vars["NUMEQU"] results.problem.number_of_nonzeros = stat_vars["NUMNZ"] results.problem.number_of_binary_variables = None # Includes binary vars: results.problem.number_of_integer_variables = stat_vars["NUMDVAR"] results.problem.number_of_continuous_variables = stat_vars["NUMVAR"] \ - stat_vars["NUMDVAR"] results.problem.number_of_objectives = 1 # required by GAMS writer obj = list(model.component_data_objects(Objective, active=True)) assert len(obj) == 1, 'Only one objective is allowed.' obj = obj[0] objctvval = stat_vars["OBJVAL"] if obj.is_minimizing(): results.problem.sense = ProblemSense.minimize results.problem.upper_bound = objctvval else: results.problem.sense = ProblemSense.maximize results.problem.lower_bound = objctvval results.solver.name = "GAMS " + str(self.version()) # Init termination condition to None to give preference to this first # block of code, only set certain TC's below if it's still None results.solver.termination_condition = None results.solver.message = None solvestat = stat_vars["SOLVESTAT"] if solvestat == 1: results.solver.status = SolverStatus.ok elif solvestat == 2: results.solver.status = SolverStatus.ok results.solver.termination_condition = TerminationCondition.maxIterations elif solvestat == 3: results.solver.status = SolverStatus.ok results.solver.termination_condition = TerminationCondition.maxTimeLimit elif solvestat == 5: results.solver.status = SolverStatus.ok results.solver.termination_condition = TerminationCondition.maxEvaluations elif solvestat == 7: results.solver.status = SolverStatus.aborted results.solver.termination_condition = TerminationCondition.licensingProblems elif solvestat == 8: results.solver.status = SolverStatus.aborted results.solver.termination_condition = TerminationCondition.userInterrupt elif solvestat == 10: results.solver.status = SolverStatus.error results.solver.termination_condition = TerminationCondition.solverFailure elif solvestat == 11: results.solver.status = SolverStatus.error results.solver.termination_condition = TerminationCondition.internalSolverError elif solvestat == 4: results.solver.status = SolverStatus.warning results.solver.message = "Solver quit with a problem (see LST file)" elif solvestat in (9, 12, 13): results.solver.status = SolverStatus.error elif solvestat == 6: results.solver.status = SolverStatus.unknown results.solver.return_code = rc # 0 # Not sure if this value is actually user time # "the elapsed time it took to execute a solve statement in total" results.solver.user_time = stat_vars["ETSOLVE"] results.solver.system_time = None results.solver.wallclock_time = None results.solver.termination_message = None soln = Solution() modelstat = stat_vars["MODELSTAT"] if modelstat == 1: results.solver.termination_condition = TerminationCondition.optimal soln.status = SolutionStatus.optimal elif modelstat == 2: results.solver.termination_condition = TerminationCondition.locallyOptimal soln.status = SolutionStatus.locallyOptimal elif modelstat in [3, 18]: results.solver.termination_condition = TerminationCondition.unbounded soln.status = SolutionStatus.unbounded elif modelstat in [4, 5, 6, 10, 19]: results.solver.termination_condition = TerminationCondition.infeasible soln.status = SolutionStatus.infeasible elif modelstat == 7: results.solver.termination_condition = TerminationCondition.feasible soln.status = SolutionStatus.feasible elif modelstat == 8: # 'Integer solution model found' results.solver.termination_condition = TerminationCondition.optimal soln.status = SolutionStatus.optimal elif modelstat == 9: results.solver.termination_condition = TerminationCondition.intermediateNonInteger soln.status = SolutionStatus.other elif modelstat == 11: # Should be handled above, if modelstat and solvestat both # indicate a licensing problem if results.solver.termination_condition is None: results.solver.termination_condition = TerminationCondition.licensingProblems soln.status = SolutionStatus.error elif modelstat in [12, 13]: if results.solver.termination_condition is None: results.solver.termination_condition = TerminationCondition.error soln.status = SolutionStatus.error elif modelstat == 14: if results.solver.termination_condition is None: results.solver.termination_condition = TerminationCondition.noSolution soln.status = SolutionStatus.unknown elif modelstat in [15, 16, 17]: # Having to do with CNS models, # not sure what to make of status descriptions results.solver.termination_condition = TerminationCondition.optimal soln.status = SolutionStatus.unsure else: # This is just a backup catch, all cases are handled above soln.status = SolutionStatus.error soln.gap = abs(results.problem.upper_bound \ - results.problem.lower_bound) model_soln = dict() # Skip first line of explanatory text for line in results_text.splitlines()[1:]: items = line.split() model_soln[items[0]] = (items[1], items[2]) has_rc_info = True for sym, ref in iteritems(symbolMap.bySymbol): obj = ref() if isinstance(model, IBlock): # Kernel variables have no 'parent_component' if obj.ctype is IObjective: soln.objective[sym] = {'Value': objctvval} if obj.ctype is not IVariable: continue else: if obj.parent_component().type() is Objective: soln.objective[sym] = {'Value': objctvval} if obj.parent_component().type() is not Var: continue rec = model_soln[sym] # obj.value = float(rec[0]) soln.variable[sym] = {"Value": float(rec[0])} if extract_rc and has_rc_info: try: # model.rc[obj] = float(rec[1]) soln.variable[sym]['rc'] = float(rec[1]) except ValueError: # Solver didn't provide marginals has_rc_info = False if extract_dual: for c in model.component_data_objects(Constraint, active=True): if (c.body.is_fixed()) or \ (not (c.has_lb() or c.has_ub())): # the constraint was not sent to GAMS continue sym = symbolMap.getSymbol(c) if c.equality: rec = model_soln[sym] try: # model.dual[c] = float(rec[1]) soln.constraint[sym] = {'dual': float(rec[1])} except ValueError: # Solver didn't provide marginals # nothing else to do here break else: # Inequality, assume if 2-sided that only # one side's marginal is nonzero # Negate marginal for _lo equations marg = 0 if c.lower is not None: rec_lo = model_soln[sym + '_lo'] try: marg -= float(rec_lo[1]) except ValueError: # Solver didn't provide marginals marg = float('nan') if c.upper is not None: rec_hi = model_soln[sym + '_hi'] try: marg += float(rec_hi[1]) except ValueError: # Solver didn't provide marginals marg = float('nan') if not math.isnan(marg): # model.dual[c] = marg soln.constraint[sym] = {'dual': marg} else: # Solver didn't provide marginals # nothing else to do here break results.solution.insert(soln) #################################################################### # Finish with results #################################################################### results._smap_id = smap_id results._smap = None if isinstance(model, IBlock): if len(results.solution) == 1: results.solution(0).symbol_map = \ getattr(model, "._symbol_maps")[results._smap_id] results.solution(0).default_variable_value = \ self._default_variable_value if load_solutions: model.load_solution(results.solution(0)) else: assert len(results.solution) == 0 # see the hack in the write method # we don't want this to stick around on the model # after the solve assert len(getattr(model, "._symbol_maps")) == 1 delattr(model, "._symbol_maps") del results._smap_id if load_solutions and \ (len(results.solution) == 0): logger.error("No solution is available") else: if load_solutions: model.solutions.load_from(results) results._smap_id = None results.solution.clear() else: results._smap = model.solutions.symbol_map[smap_id] model.solutions.delete_symbol_map(smap_id) postsolve_completion_time = time.time() if report_timing: print(" %6.2f seconds required for postsolve" % (postsolve_completion_time - solve_completion_time)) print(" %6.2f seconds required total" % (postsolve_completion_time - initial_time)) return results
def _process_soln_file(self, results, TimFile, INPUT): # # **NOTE: This solution parser assumes the baron input file # was generated by the Pyomo baron_writer plugin, and # that a dummy constraint named c_e_FIX_ONE_VAR_CONST__ # was added as the initial constraint in order to # support trivial constraint equations arrising from # fixing pyomo variables. Thus, the dual price solution # information for the first constraint in the solution # file will be excluded from the results object. # # TODO: Is there a way to handle non-zero return values from baron? # Example: the "NonLinearity Error if POW expression" # (caused by x ^ y) when both x and y are variables # causes an ugly python error and the solver log has a single # line to display the error, hard to pick out of the list # Check for suffixes to send back to pyomo extract_marginals = False extract_price = False for suffix in self._suffixes: flag = False if re.match(suffix, "rc"): #baron_marginal extract_marginals = True flag = True if re.match(suffix, "dual"): #baron_price extract_price = True flag = True if not flag: raise RuntimeError("***The BARON solver plugin cannot" "extract solution suffix="+suffix) soln = Solution() # # Process model and solver status from the Baron tim file # line = TimFile.readline().split() results.problem.name = line[0] results.problem.number_of_constraints = int(line[1]) results.problem.number_of_variables = int(line[2]) results.problem.lower_bound = float(line[5]) results.problem.upper_bound = float(line[6]) soln.gap = results.problem.upper_bound - results.problem.lower_bound solver_status = line[7] model_status = line[8] objective = None ##try: ## objective = symbol_map.getObject("__default_objective__") ## objective_label = symbol_map_byObjects[id(objective)] ##except: ## objective_label = "__default_objective__" # [JDS 17/Feb/15] I am not sure why this is needed, but all # other solvers (in particular the ASL solver and CPLEX) always # return the objective value in the __default_objective__ label, # and not by the Pyomo object name. For consistency, we will # do the same here. objective_label = "__default_objective__" soln.objective[objective_label] = {'Value': None} results.problem.number_of_objectives = 1 if objective is not None: results.problem.sense = \ 'minimizing' if objective.is_minimizing() else 'maximizing' if solver_status == '1': results.solver.status = SolverStatus.ok elif solver_status == '2': results.solver.status = SolverStatus.error results.solver.termination_condition = TerminationCondition.error #CLH: I wasn't sure if this was double reporting errors. I # just filled in one termination_message for now results.solver.termination_message = \ ("Insufficient memory to store the number of nodes required " "for this seach tree. Increase physical memory or change " "algorithmic options") elif solver_status == '3': results.solver.status = SolverStatus.ok results.solver.termination_condition = \ TerminationCondition.maxIterations elif solver_status == '4': results.solver.status = SolverStatus.ok results.solver.termination_condition = \ TerminationCondition.maxTimeLimit elif solver_status == '5': results.solver.status = SolverStatus.warning results.solver.termination_condition = \ TerminationCondition.other elif solver_status == '6': results.solver.status = SolverStatus.aborted results.solver.termination_condition = \ TerminationCondition.userInterrupt elif solver_status == '7': results.solver.status = SolverStatus.error results.solver.termination_condition = \ TerminationCondition.error elif solver_status == '8': results.solver.status = SolverStatus.unknown results.solver.termination_condition = \ TerminationCondition.unknown elif solver_status == '9': results.solver.status = SolverStatus.error results.solver.termination_condition = \ TerminationCondition.solverFailure elif solver_status == '10': results.solver.status = SolverStatus.error results.solver.termination_condition = \ TerminationCondition.error elif solver_status == '11': results.solver.status = SolverStatus.aborted results.solver.termination_condition = \ TerminationCondition.licensingProblems results.solver.termination_message = \ 'Run terminated because of a licensing error.' if model_status == '1': soln.status = SolutionStatus.optimal results.solver.termination_condition = \ TerminationCondition.optimal elif model_status == '2': soln.status = SolutionStatus.infeasible results.solver.termination_condition = \ TerminationCondition.infeasible elif model_status == '3': soln.status = SolutionStatus.unbounded results.solver.termination_condition = \ TerminationCondition.unbounded elif model_status == '4': soln.status = SolutionStatus.feasible elif model_status == '5': soln.status = SolutionStatus.unknown # # Process BARON results file # # Solutions that were preprocessed infeasible, were aborted, # or gave error will not have filled in res.lst files if results.solver.status not in [SolverStatus.error, SolverStatus.aborted]: # # Extract the solution vector and objective value from BARON # var_value = [] var_name = [] var_marginal = [] con_price = [] SolvedDuringPreprocessing = False ############# # # Scan through the first part of the solution file, until the # termination message '*** Normal completion ***' line = '' while '***' not in line: line = INPUT.readline() if 'Problem solved during preprocessing' in line: SolvedDuringPreprocessing = True INPUT.readline() INPUT.readline() try: objective_value = float(INPUT.readline().split()[4]) except IndexError: # No objective value, so no solution to return if solver_status == '1' and model_status in ('1','4'): logger.error( """Failed to process BARON solution file: could not extract the final objective value, but BARON completed normally. This is indicative of a bug in Pyomo's BARON solution parser. Please report this (along with the Pyomo model and BARON version) to the Pyomo Developers.""") return INPUT.readline() INPUT.readline() # Scan through the solution variable values line = INPUT.readline() while line.strip() != '': var_value.append(float(line.split()[2])) line = INPUT.readline() # Only scan through the marginal and price values if baron # found that information. has_dual_info = False if 'Corresponding dual solution vector is' in INPUT.readline(): has_dual_info = True INPUT.readline() line = INPUT.readline() while 'Price' not in line and line.strip() != '': var_marginal.append(float(line.split()[2])) line = INPUT.readline() if 'Price' in line: line = INPUT.readline() # # Assume the baron_writer added the dummy # c_e_FIX_ONE_VAR_CONST__ constraint as the first # line = INPUT.readline() while line.strip() != '': con_price.append(float(line.split()[2])) line = INPUT.readline() # Skip either a few blank lines or an empty block of useless # marginal and price values (if 'No dual information is available') while 'The best solution found is' not in INPUT.readline(): pass # Collect the variable names, which are given in the same # order as the lists for values already read INPUT.readline() INPUT.readline() line = INPUT.readline() while line.strip() != '': var_name.append(line.split()[0]) line = INPUT.readline() assert len(var_name) == len(var_value) # # ################ # # Plug gathered information into pyomo soln # soln_variable = soln.variable # After collecting solution information, the soln is # filled with variable name, number, and value. Also, # optionally fill the baron_marginal suffix for i, (label, val) in enumerate(zip(var_name, var_value)): soln_variable[label] = {"Value": val} # Only adds the baron_marginal key it is requested and exists if extract_marginals and has_dual_info: soln_variable[label]["rc"] = var_marginal[i] # Fill in the constraint 'price' information if extract_price and has_dual_info: soln_constraint = soln.constraint # # Assume the baron_writer added the dummy # c_e_FIX_ONE_VAR_CONST__ constraint as the first, # so constraint aliases start at 1 # for i, price_val in enumerate(con_price, 1): # use the alias made by the Baron writer con_label = ".c"+str(i) soln_constraint[con_label] = {"dual": price_val} # This check is necessary because solutions that are # preprocessed infeasible have ok solver status, but no # objective value located in the res.lst file if not (SolvedDuringPreprocessing and \ soln.status == SolutionStatus.infeasible): soln.objective[objective_label] = {'Value': objective_value} # Fill the solution for most cases, except errors results.solution.insert(soln)
def process_soln_file(self, results): # the only suffixes that we extract from CPLEX 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_rc = 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_rc = True flag = True if not flag: raise RuntimeError( "***The GUROBI solver plugin cannot extract solution suffix=" + suffix) # check for existence of the solution file # not sure why we just return - would think that we # would want to indicate some sort of error if not os.path.exists(self._soln_file): return soln = Solution() # caching for efficiency soln_variables = soln.variable soln_constraints = soln.constraint num_variables_read = 0 # string compares are too expensive, so simply introduce some # section IDs. # 0 - unknown # 1 - problem # 2 - solution # 3 - solver section = 0 # unknown solution_seen = False range_duals = {} range_slacks = {} INPUT = open(self._soln_file, "r") for line in INPUT: line = line.strip() tokens = [token.strip() for token in line.split(":")] if (tokens[0] == 'section'): if (tokens[1] == 'problem'): section = 1 elif (tokens[1] == 'solution'): section = 2 solution_seen = True elif (tokens[1] == 'solver'): section = 3 else: if (section == 2): if (tokens[0] == 'var'): if tokens[1] != "ONE_VAR_CONSTANT": soln_variables[tokens[1]] = { "Value": float(tokens[2]) } num_variables_read += 1 elif (tokens[0] == 'status'): soln.status = getattr(SolutionStatus, tokens[1]) elif (tokens[0] == 'gap'): soln.gap = float(tokens[1]) elif (tokens[0] == 'objective'): if tokens[1].strip() != 'None': soln.objective['__default_objective__'] = \ {'Value': float(tokens[1])} if results.problem.sense == ProblemSense.minimize: results.problem.upper_bound = float(tokens[1]) else: results.problem.lower_bound = float(tokens[1]) elif (tokens[0] == 'constraintdual'): name = tokens[1] if name != "c_e_ONE_VAR_CONSTANT": if name.startswith('c_'): soln_constraints.setdefault( tokens[1], {})["Dual"] = float(tokens[2]) elif name.startswith('r_l_'): range_duals.setdefault( name[4:], [0, 0])[0] = float(tokens[2]) elif name.startswith('r_u_'): range_duals.setdefault( name[4:], [0, 0])[1] = float(tokens[2]) elif (tokens[0] == 'constraintslack'): name = tokens[1] if name != "c_e_ONE_VAR_CONSTANT": if name.startswith('c_'): soln_constraints.setdefault( tokens[1], {})["Slack"] = float(tokens[2]) elif name.startswith('r_l_'): range_slacks.setdefault( name[4:], [0, 0])[0] = float(tokens[2]) elif name.startswith('r_u_'): range_slacks.setdefault( name[4:], [0, 0])[1] = float(tokens[2]) elif (tokens[0] == 'varrc'): if tokens[1] != "ONE_VAR_CONSTANT": soln_variables[tokens[1]]["Rc"] = float(tokens[2]) else: setattr(soln, tokens[0], tokens[1]) elif (section == 1): if tokens[0] == 'sense': if tokens[1] == 'minimize': results.problem.sense = ProblemSense.minimize elif tokens[1] == 'maximize': results.problem.sense = ProblemSense.maximize else: try: val = eval(tokens[1]) except: val = tokens[1] setattr(results.problem, tokens[0], val) elif (section == 3): if (tokens[0] == 'status'): results.solver.status = getattr( SolverStatus, tokens[1]) elif (tokens[0] == 'termination_condition'): try: results.solver.termination_condition = getattr( TerminationCondition, tokens[1]) except AttributeError: results.solver.termination_condition = TerminationCondition.unknown else: setattr(results.solver, tokens[0], tokens[1]) INPUT.close() # For the range constraints, supply only the dual with the largest # magnitude (at least one should always be numerically zero) for key, (ld, ud) in range_duals.items(): if abs(ld) > abs(ud): soln_constraints['r_l_' + key] = {"Dual": ld} else: # Use the same key soln_constraints['r_l_' + key] = {"Dual": ud} # slacks for key, (ls, us) in range_slacks.items(): if abs(ls) > abs(us): soln_constraints.setdefault('r_l_' + key, {})["Slack"] = ls else: # Use the same key soln_constraints.setdefault('r_l_' + key, {})["Slack"] = us if solution_seen: results.solution.insert(soln)
def process_logfile(self): """ Process a logfile """ results = SolverResults() # # Initial values # #results.solver.statistics.branch_and_bound.number_of_created_subproblems=0 #results.solver.statistics.branch_and_bound.number_of_bounded_subproblems=0 soln = Solution() soln.objective['__default_objective__'] = {'Value': None} # # Process logfile # OUTPUT = open(self._log_file) output = "".join(OUTPUT.readlines()) OUTPUT.close() # # Parse logfile lines # for line in output.split("\n"): tokens = re.split('[ \t]+', line.strip()) if len(tokens) > 3 and tokens[0] == "ABORTED:": results.solver.status = SolverStatus.aborted elif len(tokens) > 1 and tokens[0].startswith("ERROR"): results.solver.status = SolverStatus.error elif len(tokens) == 3 and tokens[0] == 'Problem' and tokens[ 2].startswith('infeasible'): results.solver.termination_condition = TerminationCondition.infeasible elif len(tokens) == 2 and tokens[0] == 'Integer' and tokens[ 1] == 'Infeasible': results.solver.termination_condition = TerminationCondition.infeasible elif len(tokens) == 5 and tokens[0] == "Final" and tokens[ 1] == "Solution:": soln.objective['__default_objective__']['Value'] = eval( tokens[4]) soln.status = SolutionStatus.optimal elif len(tokens ) == 3 and tokens[0] == "LP" and tokens[1] == "value=": soln.objective['__default_objective__']['Value'] = eval( tokens[2]) soln.status = SolutionStatus.optimal if results.problem.sense == ProblemSense.minimize: results.problem.lower_bound = eval(tokens[2]) else: results.problem.upper_bound = eval(tokens[2]) elif len(tokens) == 2 and tokens[0] == "Bound:": if results.problem.sense == ProblemSense.minimize: results.problem.lower_bound = eval(tokens[1]) else: results.problem.upper_bound = eval(tokens[1]) elif len(tokens) == 3 and tokens[0] == "Created": results.solver.statistics.branch_and_bound.number_of_created_subproblems = eval( tokens[1]) elif len(tokens) == 3 and tokens[0] == "Bounded": results.solver.statistics.branch_and_bound.number_of_bounded_subproblems = eval( tokens[1]) elif len(tokens) == 2 and tokens[0] == "sys": results.solver.system_time = eval(tokens[1]) elif len(tokens) == 2 and tokens[0] == "user": results.solver.user_time = eval(tokens[1]) elif len(tokens) == 3 and tokens[0] == "Solving" and tokens[ 1] == "problem:": results.problem.name = tokens[2] elif len(tokens) == 4 and tokens[2] == "constraints:": results.problem.number_of_constraints = eval(tokens[3]) elif len(tokens) == 4 and tokens[2] == "variables:": results.problem.number_of_variables = eval(tokens[3]) elif len(tokens) == 4 and tokens[2] == "nonzeros:": results.problem.number_of_nonzeros = eval(tokens[3]) elif len(tokens) == 3 and tokens[1] == "Sense:": if tokens[2] == "minimization": results.problem.sense = ProblemSense.minimize else: results.problem.sense = ProblemSense.maximize if results.solver.status is SolverStatus.aborted: soln.optimality = SolutionStatus.unsure if soln.status is SolutionStatus.optimal: soln.gap = 0.0 results.problem.lower_bound = soln.objective[ '__default_objective__']['Value'] results.problem.upper_bound = soln.objective[ '__default_objective__']['Value'] if soln.status == SolutionStatus.optimal: results.solver.termination_condition = TerminationCondition.optimal if not results.solver.status is SolverStatus.error and \ results.solver.termination_condition in [TerminationCondition.unknown, #TerminationCondition.maxIterations, #TerminationCondition.minFunctionValue, #TerminationCondition.minStepLength, TerminationCondition.globallyOptimal, TerminationCondition.locallyOptimal, TerminationCondition.optimal, #TerminationCondition.maxEvaluations, TerminationCondition.other]: results.solution.insert(soln) return results
def solve(self, *args, **kwds): """ Uses command line to call GAMS. tee=False: Output GAMS log to stdout. load_solutions=True: Does not support load_solutions=False. keepfiles=False: Keep temporary files. tmpdir=None: Specify directory path for storing temporary files. A directory will be created if one of this name doesn't exist. io_options: Updated with additional keywords passed to solve() warmstart=False: Warmstart by initializing model's variables to their values. symbolic_solver_labels=False: Use full Pyomo component names rather than shortened symbols (slower, but useful for debugging). labeler=None: Custom labeler. Incompatible with symbolic_solver_labels. solver=None: If None, GAMS will use default solver for model type. mtype=None: Model type. If None, will chose from lp, nlp, mip, and minlp. add_options=None: List of additional lines to write directly into model file before the solve statement. For model attributes, <model name> is GAMS_MODEL. skip_trivial_constraints=False: Skip writing constraints whose body section is fixed file_determinism=1: How much effort do we want to put into ensuring the LP file is written deterministically for a Pyomo model: 0 : None 1 : sort keys of indexed components (default) 2 : sort keys AND sort names (over declaration order) put_results='results': Not available for modification on GAMSShell solver. """ # Make sure available() doesn't crash self.available() if len(args) != 1: raise ValueError('Exactly one model must be passed ' 'to solve method of GAMSSolver.') model = args[0] load_solutions = kwds.pop("load_solutions", True) tee = kwds.pop("tee", False) keepfiles = kwds.pop("keepfiles", False) tmpdir = kwds.pop("tmpdir", None) io_options = kwds.pop("io_options", {}) if len(kwds): # Pass remaining keywords to writer, which will handle # any unrecognized arguments io_options.update(kwds) #################################################################### # Presolve #################################################################### # IMPORTANT - only delete the whole tmpdir if the solver was the one # that made the directory. Otherwise, just delete the files the solver # made, if not keepfiles. That way the user can select a directory # they already have, like the current directory, without having to # worry about the rest of the contents of that directory being deleted. newdir = False if tmpdir is None: tmpdir = mkdtemp() newdir = True elif not os.path.exists(tmpdir): # makedirs creates all necessary intermediate directories in order # to create the path to tmpdir, if they don't already exist. # However, if keepfiles is False, we only delete the final folder, # leaving the rest of the intermediate ones. os.makedirs(tmpdir) newdir = True output_filename = os.path.join(tmpdir, 'model.gms') lst_filename = os.path.join(tmpdir, 'output.lst') statresults_filename = os.path.join(tmpdir, 'resultsstat.dat') io_options['put_results'] = os.path.join(tmpdir, 'results') results_filename = os.path.join(tmpdir, 'results.dat') if isinstance(model, IBlockStorage): # Kernel blocks have slightly different write method smap_id = model.write(filename=output_filename, format=ProblemFormat.gams, _called_by_solver=True, **io_options) symbolMap = getattr(model, "._symbol_maps")[smap_id] else: (_, smap_id) = model.write(filename=output_filename, format=ProblemFormat.gams, io_options=io_options) symbolMap = model.solutions.symbol_map[smap_id] #################################################################### # Apply solver #################################################################### exe = self.executable() command = [exe, output_filename, 'o=' + lst_filename] if not tee: command.append("lo=0") try: rc = subprocess.call(command) if keepfiles: print("\nGAMS WORKING DIRECTORY: %s\n" % tmpdir) if rc == 1 or rc == 127: raise RuntimeError("Command 'gams' was not recognized") elif rc != 0: if rc == 3: # Execution Error # Run check_expr_evaluation, which errors if necessary check_expr_evaluation(model, symbolMap, 'shell') # If nothing was raised, or for all other cases, raise this raise RuntimeError("GAMS encountered an error during solve. " "Check listing file for details.") with open(results_filename, 'r') as results_file: results_text = results_file.read() with open(statresults_filename, 'r') as statresults_file: statresults_text = statresults_file.read() finally: if not keepfiles: if newdir: shutil.rmtree(tmpdir) else: os.remove(output_filename) os.remove(lst_filename) os.remove(results_filename) os.remove(statresults_filename) #################################################################### # Postsolve #################################################################### # import suffixes must be on the top-level model if isinstance(model, IBlockStorage): model_suffixes = list(name for (name,comp) \ in pyomo.core.kernel.component_suffix.\ import_suffix_generator(model, active=True, descend_into=False, return_key=True)) else: model_suffixes = list(name for (name,comp) \ in pyomo.core.base.suffix.\ active_import_suffix_generator(model)) extract_dual = ('dual' in model_suffixes) extract_rc = ('rc' in model_suffixes) stat_vars = dict() # Skip first line of explanatory text for line in statresults_text.splitlines()[1:]: items = line.split() try: stat_vars[items[0]] = float(items[1]) except ValueError: # GAMS printed NA, just make it nan stat_vars[items[0]] = float('nan') results = SolverResults() results.problem.name = output_filename results.problem.lower_bound = stat_vars["OBJEST"] results.problem.upper_bound = stat_vars["OBJEST"] results.problem.number_of_variables = stat_vars["NUMVAR"] results.problem.number_of_constraints = stat_vars["NUMEQU"] results.problem.number_of_nonzeros = stat_vars["NUMNZ"] results.problem.number_of_binary_variables = None # Includes binary vars: results.problem.number_of_integer_variables = stat_vars["NUMDVAR"] results.problem.number_of_continuous_variables = stat_vars["NUMVAR"] \ - stat_vars["NUMDVAR"] results.problem.number_of_objectives = 1 # required by GAMS writer obj = list(model.component_data_objects(Objective, active=True)) assert len(obj) == 1, 'Only one objective is allowed.' obj = obj[0] objctvval = stat_vars["OBJVAL"] if obj.is_minimizing(): results.problem.sense = ProblemSense.minimize results.problem.upper_bound = objctvval else: results.problem.sense = ProblemSense.maximize results.problem.lower_bound = objctvval results.solver.name = "GAMS " + str(self.version()) # Init termination condition to None to give preference to this first # block of code, only set certain TC's below if it's still None results.solver.termination_condition = None results.solver.message = None solvestat = stat_vars["SOLVESTAT"] if solvestat == 1: results.solver.status = SolverStatus.ok elif solvestat == 2: results.solver.status = SolverStatus.ok results.solver.termination_condition = TerminationCondition.maxIterations elif solvestat == 3: results.solver.status = SolverStatus.ok results.solver.termination_condition = TerminationCondition.maxTimeLimit elif solvestat == 5: results.solver.status = SolverStatus.ok results.solver.termination_condition = TerminationCondition.maxEvaluations elif solvestat == 7: results.solver.status = SolverStatus.aborted results.solver.termination_condition = TerminationCondition.licensingProblems elif solvestat == 8: results.solver.status = SolverStatus.aborted results.solver.termination_condition = TerminationCondition.userInterrupt elif solvestat == 10: results.solver.status = SolverStatus.error results.solver.termination_condition = TerminationCondition.solverFailure elif solvestat == 11: results.solver.status = SolverStatus.error results.solver.termination_condition = TerminationCondition.internalSolverError elif solvestat == 4: results.solver.status = SolverStatus.warning results.solver.message = "Solver quit with a problem (see LST file)" elif solvestat in (9, 12, 13): results.solver.status = SolverStatus.error elif solvestat == 6: results.solver.status = SolverStatus.unknown results.solver.return_code = rc # 0 # Not sure if this value is actually user time # "the elapsed time it took to execute a solve statement in total" results.solver.user_time = stat_vars["ETSOLVE"] results.solver.system_time = None results.solver.wallclock_time = None results.solver.termination_message = None soln = Solution() modelstat = stat_vars["MODELSTAT"] if modelstat == 1: results.solver.termination_condition = TerminationCondition.optimal soln.status = SolutionStatus.optimal elif modelstat == 2: results.solver.termination_condition = TerminationCondition.locallyOptimal soln.status = SolutionStatus.locallyOptimal elif modelstat in [3, 18]: results.solver.termination_condition = TerminationCondition.unbounded soln.status = SolutionStatus.unbounded elif modelstat in [4, 5, 6, 10, 19]: results.solver.termination_condition = TerminationCondition.infeasible soln.status = SolutionStatus.infeasible elif modelstat == 7: results.solver.termination_condition = TerminationCondition.feasible soln.status = SolutionStatus.feasible elif modelstat == 8: # 'Integer solution model found' results.solver.termination_condition = TerminationCondition.optimal soln.status = SolutionStatus.optimal elif modelstat == 9: results.solver.termination_condition = TerminationCondition.intermediateNonInteger soln.status = SolutionStatus.other elif modelstat == 11: # Should be handled above, if modelstat and solvestat both # indicate a licensing problem if results.solver.termination_condition is None: results.solver.termination_condition = TerminationCondition.licensingProblems soln.status = SolutionStatus.error elif modelstat in [12, 13]: if results.solver.termination_condition is None: results.solver.termination_condition = TerminationCondition.error soln.status = SolutionStatus.error elif modelstat == 14: if results.solver.termination_condition is None: results.solver.termination_condition = TerminationCondition.noSolution soln.status = SolutionStatus.unknown elif modelstat in [15, 16, 17]: # Having to do with CNS models, # not sure what to make of status descriptions results.solver.termination_condition = TerminationCondition.optimal soln.status = SolutionStatus.unsure else: # This is just a backup catch, all cases are handled above soln.status = SolutionStatus.error soln.gap = abs(results.problem.upper_bound \ - results.problem.lower_bound) model_soln = dict() # Skip first line of explanatory text for line in results_text.splitlines()[1:]: items = line.split() model_soln[items[0]] = (items[1], items[2]) has_rc_info = True for sym, ref in iteritems(symbolMap.bySymbol): obj = ref() if isinstance(model, IBlockStorage): # Kernel variables have no 'parent_component' if obj.ctype is Objective: soln.objective[sym] = {'Value': objctvval} if obj.ctype is not Var: continue else: if obj.parent_component().type() is Objective: soln.objective[sym] = {'Value': objctvval} if obj.parent_component().type() is not Var: continue rec = model_soln[sym] # obj.value = float(rec[0]) soln.variable[sym] = {"Value": float(rec[0])} if extract_rc and has_rc_info: try: # model.rc[obj] = float(rec[1]) soln.variable[sym]['rc'] = float(rec[1]) except ValueError: # Solver didn't provide marginals has_rc_info = False if extract_dual: for c in model.component_data_objects(Constraint, active=True): if c.body.is_fixed(): continue sym = symbolMap.getSymbol(c) if c.equality: rec = model_soln[sym] try: # model.dual[c] = float(rec[1]) soln.constraint[sym] = {'dual': float(rec[1])} except ValueError: # Solver didn't provide marginals # nothing else to do here break else: # Inequality, assume if 2-sided that only # one side's marginal is nonzero # Negate marginal for _lo equations marg = 0 if c.lower is not None: rec_lo = model_soln[sym + '_lo'] try: marg -= float(rec_lo[1]) except ValueError: # Solver didn't provide marginals marg = float('nan') if c.upper is not None: rec_hi = model_soln[sym + '_hi'] try: marg += float(rec_hi[1]) except ValueError: # Solver didn't provide marginals marg = float('nan') if not math.isnan(marg): # model.dual[c] = marg soln.constraint[sym] = {'dual': marg} else: # Solver didn't provide marginals # nothing else to do here break results.solution.insert(soln) #################################################################### # Finish with results #################################################################### results._smap_id = smap_id results._smap = None if isinstance(model, IBlockStorage): if len(results.solution) == 1: results.solution(0).symbol_map = \ getattr(model, "._symbol_maps")[results._smap_id] results.solution(0).default_variable_value = \ self._default_variable_value if load_solutions: model.load_solution(results.solution(0)) results.solution.clear() else: assert len(results.solution) == 0 # see the hack in the write method # we don't want this to stick around on the model # after the solve assert len(getattr(model, "._symbol_maps")) == 1 delattr(model, "._symbol_maps") del results._smap_id else: if load_solutions: model.solutions.load_from(results) results._smap_id = None results.solution.clear() else: results._smap = model.solutions.symbol_map[smap_id] model.solutions.delete_symbol_map(smap_id) return results
def solve(self, *args, **kwds): """ Uses GAMS Python API. For installation help visit: https://www.gams.com/latest/docs/apis/examples_python/index.html tee=False: Output GAMS log to stdout. load_solutions=True: Does not support load_solutions=False. keepfiles=False: Keep temporary files. Equivalent of DebugLevel.KeepFiles. Summary of temp files can be found in _gams_py_gjo0.pf tmpdir=None: Specify directory path for storing temporary files. A directory will be created if one of this name doesn't exist. io_options: Updated with additional keywords passed to solve() warmstart=False: Warmstart by initializing model's variables to their values. symbolic_solver_labels=False: Use full Pyomo component names rather than shortened symbols (slower, but useful for debugging). labeler=None: Custom labeler option. Incompatible with symbolic_solver_labels. solver=None: If None, GAMS will use default solver for model type. mtype=None: Model type. If None, will chose from lp, nlp, mip, and minlp. add_options=None: List of additional lines to write directly into model file before the solve statement. For model attributes, <model name> is GAMS_MODEL. skip_trivial_constraints=False: Skip writing constraints whose body section is fixed file_determinism=1: How much effort do we want to put into ensuring the LP file is written deterministically for a Pyomo model: 0 : None 1 : sort keys of indexed components (default) 2 : sort keys AND sort names (over declaration order) put_results=None: Filename for optionally writing solution values and marginals to (put_results).dat, and solver statuses to (put_results + 'stat').dat. """ # Make sure available() doesn't crash self.available() from gams import GamsWorkspace, DebugLevel from gams.workspace import GamsExceptionExecution if len(args) != 1: raise ValueError('Exactly one model must be passed ' 'to solve method of GAMSSolver.') model = args[0] load_solutions = kwds.pop("load_solutions", True) tee = kwds.pop("tee", False) keepfiles = kwds.pop("keepfiles", False) tmpdir = kwds.pop("tmpdir", None) io_options = kwds.pop("io_options", {}) if len(kwds): # Pass remaining keywords to writer, which will handle # any unrecognized arguments io_options.update(kwds) #################################################################### # Presolve #################################################################### # Create StringIO stream to pass to gams_writer, on which the # model file will be written. The writer also passes this StringIO # back, but output_file is defined in advance for clarity. output_file = StringIO() if isinstance(model, IBlockStorage): # Kernel blocks have slightly different write method smap_id = model.write(filename=output_file, format=ProblemFormat.gams, _called_by_solver=True, **io_options) symbolMap = getattr(model, "._symbol_maps")[smap_id] else: (_, smap_id) = model.write(filename=output_file, format=ProblemFormat.gams, io_options=io_options) symbolMap = model.solutions.symbol_map[smap_id] #################################################################### # Apply solver #################################################################### # IMPORTANT - only delete the whole tmpdir if the solver was the one # that made the directory. Otherwise, just delete the files the solver # made, if not keepfiles. That way the user can select a directory # they already have, like the current directory, without having to # worry about the rest of the contents of that directory being deleted. newdir = True if tmpdir is not None and os.path.exists(tmpdir): newdir = False ws = GamsWorkspace( debug=DebugLevel.KeepFiles if keepfiles else DebugLevel.Off, working_directory=tmpdir) t1 = ws.add_job_from_string(output_file.getvalue()) try: t1.run(output=sys.stdout if tee else None) except GamsExceptionExecution: try: check_expr_evaluation(model, symbolMap, 'direct') finally: # Always name working directory or delete files, # regardless of any errors. if keepfiles: print("\nGAMS WORKING DIRECTORY: %s\n" % ws.working_directory) elif tmpdir is not None: # Garbage collect all references to t1.out_db # So that .gdx file can be deleted t1 = rec = rec_lo = rec_hi = None file_removal_gams_direct(tmpdir, newdir) raise except: # Catch other errors and remove files first if keepfiles: print("\nGAMS WORKING DIRECTORY: %s\n" % ws.working_directory) elif tmpdir is not None: # Garbage collect all references to t1.out_db # So that .gdx file can be deleted t1 = rec = rec_lo = rec_hi = None file_removal_gams_direct(tmpdir, newdir) raise #################################################################### # Postsolve #################################################################### # import suffixes must be on the top-level model if isinstance(model, IBlockStorage): model_suffixes = list(name for (name,comp) \ in pyomo.core.kernel.component_suffix.\ import_suffix_generator(model, active=True, descend_into=False, return_key=True)) else: model_suffixes = list(name for (name,comp) \ in pyomo.core.base.suffix.\ active_import_suffix_generator(model)) extract_dual = ('dual' in model_suffixes) extract_rc = ('rc' in model_suffixes) results = SolverResults() results.problem.name = t1.name results.problem.lower_bound = t1.out_db["OBJEST"].find_record().value results.problem.upper_bound = t1.out_db["OBJEST"].find_record().value results.problem.number_of_variables = \ t1.out_db["NUMVAR"].find_record().value results.problem.number_of_constraints = \ t1.out_db["NUMEQU"].find_record().value results.problem.number_of_nonzeros = \ t1.out_db["NUMNZ"].find_record().value results.problem.number_of_binary_variables = None # Includes binary vars: results.problem.number_of_integer_variables = \ t1.out_db["NUMDVAR"].find_record().value results.problem.number_of_continuous_variables = \ t1.out_db["NUMVAR"].find_record().value \ - t1.out_db["NUMDVAR"].find_record().value results.problem.number_of_objectives = 1 # required by GAMS writer obj = list(model.component_data_objects(Objective, active=True)) assert len(obj) == 1, 'Only one objective is allowed.' obj = obj[0] objctvval = t1.out_db["OBJVAL"].find_record().value if obj.is_minimizing(): results.problem.sense = ProblemSense.minimize results.problem.upper_bound = objctvval else: results.problem.sense = ProblemSense.maximize results.problem.lower_bound = objctvval results.solver.name = "GAMS " + str(self.version()) # Init termination condition to None to give preference to this first # block of code, only set certain TC's below if it's still None results.solver.termination_condition = None results.solver.message = None solvestat = t1.out_db["SOLVESTAT"].find_record().value if solvestat == 1: results.solver.status = SolverStatus.ok elif solvestat == 2: results.solver.status = SolverStatus.ok results.solver.termination_condition = TerminationCondition.maxIterations elif solvestat == 3: results.solver.status = SolverStatus.ok results.solver.termination_condition = TerminationCondition.maxTimeLimit elif solvestat == 5: results.solver.status = SolverStatus.ok results.solver.termination_condition = TerminationCondition.maxEvaluations elif solvestat == 7: results.solver.status = SolverStatus.aborted results.solver.termination_condition = TerminationCondition.licensingProblems elif solvestat == 8: results.solver.status = SolverStatus.aborted results.solver.termination_condition = TerminationCondition.userInterrupt elif solvestat == 10: results.solver.status = SolverStatus.error results.solver.termination_condition = TerminationCondition.solverFailure elif solvestat == 11: results.solver.status = SolverStatus.error results.solver.termination_condition = TerminationCondition.internalSolverError elif solvestat == 4: results.solver.status = SolverStatus.warning results.solver.message = "Solver quit with a problem (see LST file)" elif solvestat in (9, 12, 13): results.solver.status = SolverStatus.error elif solvestat == 6: results.solver.status = SolverStatus.unknown results.solver.return_code = 0 # Not sure if this value is actually user time # "the elapsed time it took to execute a solve statement in total" results.solver.user_time = t1.out_db["ETSOLVE"].find_record().value results.solver.system_time = None results.solver.wallclock_time = None results.solver.termination_message = None soln = Solution() modelstat = t1.out_db["MODELSTAT"].find_record().value if modelstat == 1: results.solver.termination_condition = TerminationCondition.optimal soln.status = SolutionStatus.optimal elif modelstat == 2: results.solver.termination_condition = TerminationCondition.locallyOptimal soln.status = SolutionStatus.locallyOptimal elif modelstat in [3, 18]: results.solver.termination_condition = TerminationCondition.unbounded soln.status = SolutionStatus.unbounded elif modelstat in [4, 5, 6, 10, 19]: results.solver.termination_condition = TerminationCondition.infeasible soln.status = SolutionStatus.infeasible elif modelstat == 7: results.solver.termination_condition = TerminationCondition.feasible soln.status = SolutionStatus.feasible elif modelstat == 8: # 'Integer solution model found' results.solver.termination_condition = TerminationCondition.optimal soln.status = SolutionStatus.optimal elif modelstat == 9: results.solver.termination_condition = TerminationCondition.intermediateNonInteger soln.status = SolutionStatus.other elif modelstat == 11: # Should be handled above, if modelstat and solvestat both # indicate a licensing problem if results.solver.termination_condition is None: results.solver.termination_condition = TerminationCondition.licensingProblems soln.status = SolutionStatus.error elif modelstat in [12, 13]: if results.solver.termination_condition is None: results.solver.termination_condition = TerminationCondition.error soln.status = SolutionStatus.error elif modelstat == 14: if results.solver.termination_condition is None: results.solver.termination_condition = TerminationCondition.noSolution soln.status = SolutionStatus.unknown elif modelstat in [15, 16, 17]: # Having to do with CNS models, # not sure what to make of status descriptions results.solver.termination_condition = TerminationCondition.optimal soln.status = SolutionStatus.unsure else: # This is just a backup catch, all cases are handled above soln.status = SolutionStatus.error soln.gap = abs(results.problem.upper_bound \ - results.problem.lower_bound) for sym, ref in iteritems(symbolMap.bySymbol): obj = ref() if isinstance(model, IBlockStorage): # Kernel variables have no 'parent_component' if obj.ctype is Objective: soln.objective[sym] = {'Value': objctvval} if obj.ctype is not Var: continue else: if obj.parent_component().type() is Objective: soln.objective[sym] = {'Value': objctvval} if obj.parent_component().type() is not Var: continue rec = t1.out_db[sym].find_record() # obj.value = rec.level soln.variable[sym] = {"Value": rec.level} if extract_rc and not math.isnan(rec.marginal): # Do not set marginals to nan # model.rc[obj] = rec.marginal soln.variable[sym]['rc'] = rec.marginal if extract_dual: for c in model.component_data_objects(Constraint, active=True): if c.body.is_fixed(): continue sym = symbolMap.getSymbol(c) if c.equality: rec = t1.out_db[sym].find_record() if not math.isnan(rec.marginal): # model.dual[c] = rec.marginal soln.constraint[sym] = {'dual': rec.marginal} else: # Solver didn't provide marginals, # nothing else to do here break else: # Inequality, assume if 2-sided that only # one side's marginal is nonzero # Negate marginal for _lo equations marg = 0 if c.lower is not None: rec_lo = t1.out_db[sym + '_lo'].find_record() marg -= rec_lo.marginal if c.upper is not None: rec_hi = t1.out_db[sym + '_hi'].find_record() marg += rec_hi.marginal if not math.isnan(marg): # model.dual[c] = marg soln.constraint[sym] = {'dual': marg} else: # Solver didn't provide marginals, # nothing else to do here break results.solution.insert(soln) if keepfiles: print("\nGAMS WORKING DIRECTORY: %s\n" % ws.working_directory) elif tmpdir is not None: # Garbage collect all references to t1.out_db # So that .gdx file can be deleted t1 = rec = rec_lo = rec_hi = None file_removal_gams_direct(tmpdir, newdir) #################################################################### # Finish with results #################################################################### results._smap_id = smap_id results._smap = None if isinstance(model, IBlockStorage): if len(results.solution) == 1: results.solution(0).symbol_map = \ getattr(model, "._symbol_maps")[results._smap_id] results.solution(0).default_variable_value = \ self._default_variable_value if load_solutions: model.load_solution(results.solution(0)) results.solution.clear() else: assert len(results.solution) == 0 # see the hack in the write method # we don't want this to stick around on the model # after the solve assert len(getattr(model, "._symbol_maps")) == 1 delattr(model, "._symbol_maps") del results._smap_id else: if load_solutions: model.solutions.load_from(results) results._smap_id = None results.solution.clear() else: results._smap = model.solutions.symbol_map[smap_id] model.solutions.delete_symbol_map(smap_id) return results