# ___________________________________________________________________________ # # Pyomo: Python Optimization Modeling Objects # Copyright 2017 National Technology and Engineering Solutions of Sandia, LLC # Under the terms of Contract DE-NA0003525 with National Technology and # Engineering Solutions of Sandia, LLC, the U.S. Government retains certain # rights in this software. # This software is distributed under the 3-clause BSD License. # ___________________________________________________________________________ from __future__ import division import os import logging import copy from six import iteritems from operator import itemgetter from pyomo.pysp.phutils import indexToString from pyomo.opt import UndefinedData logger = logging.getLogger('pyomo.pysp') # ===== various ways to extract xhat ======= def ExtractInternalNodeSolutionsWithClosestScenarioNodebyNode(ph): # find the scenario closest to xbar at each node and return it node_solutions = {} def ScenXbarDist(scen, tree_node): # crude estimate of stdev to get a approx, truncated z score dist = 0 xbars = tree_node._xbars mins = tree_node._minimums maxs = tree_node._maximums for variable_id in tree_node._standard_variable_ids: diff = scen._x[tree_node._name][variable_id] - xbars[variable_id] s_est = (maxs[variable_id] - mins[variable_id]) / 4.0 # close enough to stdev if s_est > ph._integer_tolerance: dist += min(3, abs(diff)/s_est) # truncated z score return dist ClosestScen = None ClosestScenDist = None for stage in ph._scenario_tree._stages[:-1]: for tree_node in stage._tree_nodes: this_node_sol = node_solutions[tree_node._name] = {} for scenario in tree_node._scenarios: if ClosestScenDist == 0: break thisdist = ScenXbarDist(scenario, tree_node) if ClosestScenDist == None or thisdist < ClostScenDist: ClosestScendist = thisdist ClosestScen = scenario for variable_id in tree_node._standard_variable_ids: ## print ("extracting for "+str(variable_id)+" the value "+str(ClosestScen._x[tree_node._name][variable_id])) this_node_sol[variable_id] = ClosestScen._x[tree_node._name][variable_id] return node_solutions def ExtractInternalNodeSolutionsWithDiscreteRounding(ph): node_solutions = {} for stage in ph._scenario_tree._stages[:-1]: for tree_node in stage._tree_nodes: this_node_sol = node_solutions[tree_node._name] = {} xbars = tree_node._xbars for variable_id in tree_node._standard_variable_ids: if not tree_node.is_variable_discrete(variable_id): this_node_sol[variable_id] = xbars[variable_id] else: # rounded xbar, which has a MUCH # better chance of being feasible this_node_sol[variable_id] = \ int(round(xbars[variable_id])) return node_solutions def ExtractInternalNodeSolutionsWithDiscreteVoting(ph): node_solutions = {} for stage in ph._scenario_tree._stages[:-1]: for tree_node in stage._tree_nodes: this_node_sol = node_solutions[tree_node._name] = {} xbars = tree_node._xbars for variable_id in tree_node._standard_variable_ids: if not tree_node.is_variable_discrete(variable_id): this_node_sol[variable_id] = xbars[variable_id] else: # for discrete variables use a weighted vote # Note: for binary this can just be computed # by rounding the xbar (assuming it's an # average). However, the following # works for binary and general integer, # where rounding the average is not # necessarily the same as a weighted vote # outcome. vals = [int(round(scenario._x[tree_node._name][variable_id]))\ for scenario in tree_node._scenarios] bins = list(set(vals)) vote = [] for val in bins: vote.append(sum(scenario._probability \ for scenario in tree_node._scenarios \ if int(round(scenario._x[tree_node._name][variable_id])) == val)) # assign the vote outcome this_node_sol[variable_id] = bins[vote.index(max(vote))] return node_solutions def ExtractInternalNodeSolutionsWithSlamming(ph): from pyomo.pysp.plugins.wwphextension import _parse_yaml_file # Since it was a file, # assume that the argument was a json file with slamming instructions. # This will ignore suffixes we don't care about. # If there are no instructions use xbar. # Note: there is an implicit pecking order. print ("For x-hat, using slamming suffixes in",ph._xhat_method) slamdict = {} for suffix_name, suffix_value, variable_ids in \ _parse_yaml_file(ph, ph._xhat_method): for node_name, node_variable_ids in iteritems(variable_ids): for variable_id in node_variable_ids: if variable_id not in slamdict: slamdict[variable_id] = {} slamdict[variable_id][suffix_name] = suffix_value verbose = ph._verbose node_solutions = {} for stage in ph._scenario_tree._stages[:-1]: for tree_node in stage._tree_nodes: this_node_sol = node_solutions[tree_node._name] = {} xbars = tree_node._xbars mins = tree_node._minimums maxs = tree_node._maximums warnb = False # did the user do something less than cool? for variable_id in tree_node._standard_variable_ids: if verbose: variable_name, index = tree_node._variable_ids[variable_id] full_variable_name = variable_name+indexToString(index) print ("Setting x-hat for",full_variable_name) if variable_id not in slamdict or slamdict[variable_id]['CanSlamToAnywhere']: if not tree_node.is_variable_discrete(variable_id): this_node_sol[variable_id] = xbars[variable_id] if verbose: print (" x-bar", this_node_sol[variable_id]) else: this_node_sol[variable_id] = int(round(xbars[variable_id])) if verbose: print (" rounded x-bar", this_node_sol[variable_id]) elif slamdict[variable_id]['CanSlamToMin']: this_node_sol[variable_id] = mins[variable_id] if verbose: print (" min over scenarios", this_node_sol[variable_id]) elif slamdict[variable_id]['CanSlamToMax']: this_node_sol[variable_id] = maxs[variable_id] if verbose: print (" max over scenarios", this_node_sol[variable_id]) elif slamdict[variable_id]['CanSlamToLB']: warnb = True this_node_sol[variable_id] = mins[variable_id] if verbose: print (" Lower Bound", this_node_sol[variable_id]) elif slamdict[variable_id]['CanSlamToUB']: warnb = True this_node_sol[variable_id] = maxs[variable_id] if verbose: print (" Upper Bound", this_node_sol[variable_id]) if warnb: print ("Warning: for xhat determination from file %s, some variables had an upper or lower bound slam but not a corresponding min or max", ph._xhat_method) return node_solutions #### call one #### def ExtractInternalNodeSolutionsforInner(ph): # get xhat for the inner bound (upper bound for a min problem) if ph._xhat_method == "closest-scenario": return ExtractInternalNodeSolutionsWithClosestScenarioNodebyNode(ph) elif ph._xhat_method == "voting": return ExtractInternalNodeSolutionsWithDiscreteVoting(ph) elif ph._xhat_method == "rounding": return ExtractInternalNodeSolutionsWithDiscreteRounding(ph) elif os.path.isfile(ph._xhat_method): return ExtractInternalNodeSolutionsWithSlamming(ph) else: raise RuntimeError("Unknown x-hat determination method=%s specified in PH - unable to extract candidate solution for inner bound computation" % ph._xhat_method) #============== end xhat code ========== class _PHBoundBase(object): # Nothing interesting STATUS_NONE = 0b0000 # Used mipgap STATUS_MIPGAP = 0b0001 # Solution gap was not reported STATUS_GAP_NA = 0b0010 # Solution has nonzero # optimality gap STATUS_GAP_NONZERO = 0b0100 # One or more subproblems were infeasible STATUS_SOLVE_FAILED = 0b1000 WARNING_MESSAGE = {} # No mipgap detected, but a # nonzero solution gap was # found WARNING_MESSAGE[0b0100] = \ "** Possibly Conservative - Mipgap Unknown, And Nonzero Solution Gap Reported **" # Used mipgap and solver did # report a solution gap WARNING_MESSAGE[0b0101] = \ "** Possibly Conservative - Mipgap Detected, And Nonzero Solution Gap Reported **" # Used mipgap and solver did NOT # report a solution gap WARNING_MESSAGE[0b0111] = \ "** Extreme Caution - Mipgap Detected, But No Solution Gap Information Obtained - Bound May Be Invalid **" WARNING_MESSAGE[0b0011] = \ "** Extreme Caution - Mipgap Detected, But No Solution Gap Information Obtained - Bound May Be Invalid **" WARNING_MESSAGE[0b0110] = \ "** Caution - Solver Did Not Provide Solution Gap Information - Bound May Be Invalid **" WARNING_MESSAGE[0b0010] = \ "** Caution - Solver Did Not Provide Solution Gap Information - Bound May Be Invalid **" # Tags for operations for which we need to undo in order to return # ph to its original state SOLUTION_CACHING = (1,) VARIABLE_FREEING = (2,) DEACTIVATE_PROXIMAL = (3,) DEACTIVATE_WEIGHT = (4,) CACHE_WEIGHTS = (5,) TREE_VARIABLE_FIXING = (6,) def __init__(self): # the interval for which bound computations are performed # during the ph iteration k solves. A bound update is always # performed at iteration 0 with the (non-PH-augmented) # objective and the update interval starts with a bound update # at ph iteration 1. self._update_interval = 1 # keys are ph iteration except for the trival bound whose key # is None self._outer_bound_history = {} self._outer_status_history = {} self._inner_bound_history = {} self._inner_status_history = {} self._is_minimizing = True # stack of operations modifying ph self._stack = [] def RestoreLastPHChange(self, ph): if len(self._stack): tmp_restore_stack = [self._stack.pop()] real_stack = self._stack self._stack = tmp_restore_stack self.RestorePH(ph) self._stack = real_stack def RestorePH(self, ph): while self._stack: op, op_data = self._stack.pop() if op == self.SOLUTION_CACHING: ph.restoreCachedSolutions(op_data, release_cache=True) elif op == self.VARIABLE_FREEING: ph_fixed, ph_fix_queue = op_data # Refix all previously fixed variables for tree_node in ph._scenario_tree._tree_nodes: for variable_id, fix_value in iteritems(ph_fixed[tree_node._name]): tree_node.fix_variable(variable_id, fix_value) # Push fixed variable statuses to instances (or # transmit to the phsolverservers) ph._push_fix_queue_to_instances() # Restore the fix_queue for tree_node in ph._scenario_tree._tree_nodes: tree_node._fix_queue.update( ph_fix_queue[tree_node._name]) elif op == self.TREE_VARIABLE_FIXING: ph_fixed, ph_fix_queue = op_data # Unfix all non-leaf variables that were fixed to xbar for stage in ph._scenario_tree._stages[:-1]: for tree_node in stage._tree_nodes: for variable_id in tree_node._standard_variable_ids: tree_node.free_variable(variable_id) # Refix all previously fixed variables for tree_node in ph._scenario_tree._tree_nodes: for variable_id, fix_value in iteritems(ph_fixed[tree_node._name]): tree_node.fix_variable(variable_id, fix_value) # Push fixed variable statuses to instances (or # transmit to the phsolverservers) ph._push_fix_queue_to_instances() # Restore the fix_queue for tree_node in ph._scenario_tree._tree_nodes: tree_node._fix_queue.update( ph_fix_queue[tree_node._name]) elif op == self.DEACTIVATE_PROXIMAL: assert op_data == None ph.activate_ph_objective_proximal_terms() elif op == self.DEACTIVATE_WEIGHT: assert op_data == None ph.activate_ph_objective_weight_terms() elif op == self.CACHE_WEIGHTS: weights = op_data for scenario in ph._scenario_tree._scenarios: cached_weights = weights[scenario._name] scenario._w = copy.deepcopy(cached_weights) else: raise ValueError("Unexpected Operation Flag - " "This is a developer error") def CachePHSolution(self, ph): # Cache the current state of the ph instances and scenario # tree so we don't affect ph behavior by changing the solution # on the scenario tree / scenario instances prior to the next # solve cache_id = ph.cacheSolutions() self._stack.append((self.SOLUTION_CACHING, cache_id)) def RelaxPHFixedVariables(self, ph): # Save the current fixed state and fix queue ph_fixed = dict((tree_node._name, copy.deepcopy(tree_node._fixed)) \ for tree_node in ph._scenario_tree._tree_nodes) ph_fix_queue = \ dict((tree_node._name, copy.deepcopy(tree_node._fix_queue)) \ for tree_node in ph._scenario_tree._tree_nodes) # Free all currently fixed variables for tree_node in ph._scenario_tree._tree_nodes: tree_node.clear_fix_queue() for variable_id in ph_fixed[tree_node._name]: tree_node.free_variable(variable_id) # Push freed variable statuses on instances (or # transmit to the phsolverservers) ph._push_fix_queue_to_instances() self._stack.append((self.VARIABLE_FREEING, (ph_fixed, ph_fix_queue))) def FixScenarioTreeVariables(self, ph, fix_values): # Save the current fixed state and fix queue and clear the fix queue ph_fixed = {} ph_fix_queue = {} for tree_node in ph._scenario_tree._tree_nodes: ph_fixed[tree_node._name] = copy.deepcopy(tree_node._fixed) ph_fix_queue[tree_node._name] = copy.deepcopy(tree_node._fix_queue) tree_node.clear_fix_queue() # Fix everything in fix_values for node_name in fix_values: tree_node = ph._scenario_tree._tree_node_map[node_name] for variable_id, fix_val in iteritems(fix_values[node_name]): tree_node.fix_variable(variable_id, fix_val) # Push fixed variable statuses on instances (or # transmit to the phsolverservers) ph._push_fix_queue_to_instances() self._stack.append((self.TREE_VARIABLE_FIXING, (ph_fixed, ph_fix_queue))) def DeactivatePHObjectiveProximalTerms(self, ph): ph.deactivate_ph_objective_proximal_terms() self._stack.append((self.DEACTIVATE_PROXIMAL, None)) def DeactivatePHObjectiveWeightTerms(self, ph): ph.deactivate_ph_objective_weight_terms() self._stack.append((self.DEACTIVATE_WEIGHT, None)) def CachePHWeights(self, ph): weights = {} for scenario in ph._scenario_tree._scenarios: weights[scenario._name] = \ copy.deepcopy(scenario._w) self._stack.append((self.CACHE_WEIGHTS,weights)) # # Calculates the probability weighted sum of all suproblem (or # bundle) objective functions. This function assumes the current # subproblem solutions, in particular the objective values stored # on the scenario objects, are appropriate for computing an outer # bound on the true optimal objective value (e.g., lower bound for # minimization problem). When a nonzero optimality gap is reported # in a scenario/bundle solution, the reported objective value for # that scenario/bundle will be relaxed accordingly before # including it in the average calculation. # def ComputeOuterBound(self, ph, storage_key): if ph._verbose: print("Computing objective %s bound" % ("outer" if self._is_minimizing else "inner")) bound_status = self.STATUS_NONE if (ph._mipgap is not None) and (ph._mipgap > 0): logger.warning("A nonzero mipgap was detected when using " "the PH bound plugin. The bound " "computation may as a result be conservative.") bound_status |= self.STATUS_MIPGAP objective_bound = 0.0 # If we are bundling, we compute the objective bound in a way # such that we can still use solution gap information if it # is available. if ph._scenario_tree.contains_bundles(): for scenario_bundle in ph._scenario_tree._scenario_bundles: bundle_gap = ph._gaps[scenario_bundle._name] bundle_objective_value = 0.0 for scenario in scenario_bundle._scenario_tree._scenarios: # The objective must be taken from the scenario # objects on PH full scenario tree scenario_objective = \ ph._scenario_tree.get_scenario(scenario._name)._objective # And we need to make sure to use the # probabilities assigned to scenarios in the # compressed bundle scenario tree bundle_objective_value += scenario_objective * \ scenario._probability if not isinstance(bundle_gap, UndefinedData): if bundle_gap > 0: bound_status |= self.STATUS_GAP_NONZERO if self._is_minimizing: bundle_objective_value -= bundle_gap else: bundle_objective_value += bundle_gap else: bound_status |= self.STATUS_GAP_NA objective_bound += bundle_objective_value * \ scenario_bundle._probability else: for scenario in ph._scenario_tree._scenarios: this_objective_value = scenario._objective this_gap = ph._gaps[scenario._name] if not isinstance(this_gap, UndefinedData): if this_gap > 0: bound_status |= self.STATUS_GAP_NONZERO if self._is_minimizing: this_objective_value -= this_gap else: this_objective_value += this_gap else: bound_status |= self.STATUS_GAP_NA objective_bound += (scenario._probability * this_objective_value) if ph._verbose: print("Computed objective %s bound=%12.4f\t%s" % (("outer" if self._is_minimizing else "inner"), objective_bound, self.WARNING_MESSAGE.get(bound_status,""))) return objective_bound, bound_status # # Calculates the probability weighted sum of all suproblem (or # bundle) objective functions. This function assumes the current # subproblem solutions represent valid, non-anticpative solutions # are appropriate for computing an inner bound on the optimal # objective value (e.g., upper bound for minimization problem). # def ComputeInnerBound(self, ph, storage_key): if ph._verbose: print("Computing objective %s bound" % ("inner" if self._is_minimizing else "outer")) objective_bound = 0.0 for scenario in ph._scenario_tree._scenarios: objective_bound += (scenario._probability * scenario._objective) if ph._verbose: print("Computed objective %s bound=%12.4f" % (("inner" if self._is_minimizing else "outer"), objective_bound)) return objective_bound, self.STATUS_NONE def ReportBestBound(self): print("") best_inner_bound = None if len(self._inner_bound_history) > 0: # dlw May 2016 if self._is_minimizing: best_inner_bound = min(self._inner_bound_history.values()) else: best_inner_bound = max(self._inner_bound_history.values()) print("Best Incumbent Bound: %15s" % (best_inner_bound)) best_bound = None if len(self._outer_bound_history) > 0: if self._is_minimizing: best_bound_key, best_bound = max(self._outer_bound_history.items(), key=itemgetter(1)) else: best_bound_key, best_bound = min(self._outer_bound_history.items(), key=itemgetter(1)) print("Best Dual Bound: %15s\t%s" % (best_bound, self.WARNING_MESSAGE.get(self._outer_status_history[best_bound_key],""))) print("Absolute Duality Gap: %15s" % abs(best_inner_bound - best_bound)) relgap = float('inf') if (best_inner_bound != float('inf')) and \ (best_inner_bound != float('-inf')) and \ (best_bound != float('-inf')) and \ (best_bound != float('-inf')): relgap = abs(best_inner_bound - best_bound) / \ (1e-10+abs(best_bound)) print("Relative Gap: %15s %s" % (relgap*100.0,"%")) print("") output_filename = "phbestbound.txt" output_file = open(output_filename,"w") output_file.write("Incumbent: %.17g\n" % best_inner_bound) output_file.write("Dual: %.17g\n" % best_bound) output_file.close() print("Best bound written to file="+output_filename) def ReportBoundHistory(self): print("") print("Bound History") print("%15s %15s %15s" % ("Iteration", "Inner Bound", "Outer Bound")) keys = list(self._outer_bound_history.keys()) if None in keys: keys.remove(None) print("%15s %15s %15s\t\t%s" % ("Trivial", " - ", self._outer_bound_history[None], self.WARNING_MESSAGE.get(self._outer_status_history[None],""))) for key in sorted(keys): print("%15s %15s %15s\t\t%s" % (key, self._inner_bound_history[key], self._outer_bound_history[key], self.WARNING_MESSAGE.get(self._outer_status_history[key],""))) print("") output_filename = "phbound.txt" with open(output_filename,"w") as output_file: output_file.write('Inner Bound:\n') for key in sorted(self._inner_bound_history.keys()): output_file.write(" %d: %.17g\n" % (key, self._inner_bound_history[key])) output_file.write('Outer Bound:\n') if None in self._outer_bound_history: output_file.write(" Trivial: %.17g\n" % (self._outer_bound_history[None])) for key in sorted(keys): output_file.write(" %d: %.17g\n" % (key, self._outer_bound_history[key])) print("Bound history written to file="+output_filename)