def Eobjective(self, verbose=False): """ Compute the expected objective function across all scenarios. Note: Assumes the optimization is done beforehand, therefore DOES NOT CHECK FEASIBILITY or NON-ANTICIPATIVITY! This method uses whatever the current value of the objective function is. Args: verbose (boolean, optional): If True, displays verbose output. Default False. Returns: float: The expected objective function value """ local_Eobjs = [] for k, s in self.local_scenarios.items(): if self.bundling: objfct = self.saved_objs[k] else: objfct = sputils.find_active_objective(s) local_Eobjs.append(s._mpisppy_probability * pyo.value(objfct)) if verbose: print("caller", inspect.stack()[1][3]) print ("E_Obj Scenario {}, prob={}, Obj={}, ObjExpr={}"\ .format(k, s._mpisppy_probability, pyo.value(objfct), objfct.expr)) local_Eobj = np.array([math.fsum(local_Eobjs)]) global_Eobj = np.zeros(1) self.mpicomm.Allreduce(local_Eobj, global_Eobj, op=MPI.SUM) return global_Eobj[0]
def solve_one(self, solver_options, k, s, dtiming=False, gripe=False, tee=False, verbose=False, disable_pyomo_signal_handling=False, update_objective=True, compute_val_at_nonant=False): self._lazy_create_solvers() pyomo_solve_time = super().solve_one( solver_options, k, s, dtiming=dtiming, gripe=gripe, tee=tee, verbose=verbose, disable_pyomo_signal_handling=disable_pyomo_signal_handling, update_objective=update_objective) if compute_val_at_nonant: if self.bundling: objfct = self.saved_objs[k] else: objfct = sputils.find_active_objective(s) if self.verbose: print("caller", inspect.stack()[1][3]) print ("E_Obj Scenario {}, prob={}, Obj={}, ObjExpr={}"\ .format(k, s._mpisppy_probability, pyo.value(objfct), objfct.expr)) self.objs_dict[k] = pyo.value(objfct) return (pyomo_solve_time)
def _extract_objective(self, mip): ''' Extract the original part of the provided MIP's objective function (no dual or prox terms), and create a copy containing the QP variables in place of the MIP variables. Args: mip (Pyomo ConcreteModel): MIP model for a scenario or bundle. Returns: obj (Pyomo Objective): objective function extracted from the MIP new (Pyomo Expression): expression from the MIP model objective with the MIP variables replaced by QP variables. Does not inculde dual or prox terms. Notes: Acts on either a single-scenario model or a bundle ''' mip_to_qp = mip._mpisppy_data.mip_to_qp obj = find_active_objective(mip) repn = generate_standard_repn(obj.expr, quadratic=True) if len(repn.nonlinear_vars) > 0: raise ValueError("FWPH does not support models with nonlinear objective functions") linear_vars = [mip_to_qp[id(var)] for var in repn.linear_vars] new = LinearExpression( constant=repn.constant, linear_coefs=repn.linear_coefs, linear_vars=linear_vars ) if repn.quadratic_vars: quadratic_vars = ( (mip_to_qp[id(x)], mip_to_qp[id(y)]) for x,y in repn.quadratic_vars ) new += pyo.quicksum( (coef*x*y for coef,(x,y) in zip(repn.quadratic_coefs, quadratic_vars)) ) return obj, new
def build_model_for_scenario(self, scenario_identifier: str) -> Tuple[_BlockData, Dict[Any, _GeneralVarData]]: m = self.local_scenario_models[scenario_identifier] _assert_continuous(m) active_obj = find_active_objective(m) active_obj.deactivate() m._mpisppy_model.weighted_obj = pyo.Objective(expr=m._mpisppy_probability * active_obj.expr, sense=active_obj.sense) nonant_vars = m._mpisppy_data.nonant_indices if len(nonant_vars) != len(self.nonant_vars): raise ValueError(f'Number of non-anticipative variables is not consistent in scenario {scenario_identifier}.') return m, nonant_vars
def write_loop(self): """ Bundles are special. Also: this code needs help from the ph object to be more efficient... """ for sname, s in self.ph.local_scenarios.items(): bundling = self.ph.bundling fname = self.dirname + os.sep + sname + ".dag" with open(fname, "a") as f: f.write(str(self.ph._PHIter) + ",") if not bundling: objfct = find_active_objective(s) f.write(str(pyo.value(objfct))) else: f.write("Bundling" + ",") f.write(str(pyo.value(self.ph.saved_objs[sname]))) f.write("\n")
def _check_bound(self): opt = self.opt chached_ph_obj = dict() for k,s in opt.local_subproblems.items(): phobj = find_active_objective(s) phobj.deactivate() chached_ph_obj[k] = phobj s._mpisppy_model.EF_Obj.activate() teeme = ( "tee-rank0-solves" in opt.PHoptions and opt.PHoptions["tee-rank0-solves"] ) opt.solve_loop( solver_options=opt.current_solver_options, dtiming=opt.PHoptions["display_timing"], gripe=True, disable_pyomo_signal_handling=False, tee=teeme, verbose=opt.PHoptions["verbose"], ) local_obs = np.fromiter((s._mpisppy_data.outer_bound for s in opt.local_subproblems.values()), dtype="d", count=len(opt.local_subproblems)) local_ob = np.empty(1) if opt.is_minimizing: local_ob[0] = local_obs.max() else: local_ob[0] = local_obs.min() global_ob = np.empty(1) if opt.is_minimizing: opt.mpicomm.Allreduce(local_ob, global_ob, op=mpi.MAX) else: opt.mpicomm.Allreduce(local_ob, global_ob, op=mpi.MIN) #print(f"CrossScenarioExtension OB: {global_ob[0]}") opt.spcomm.BestOuterBound = opt.spcomm.OuterBoundUpdate(global_ob[0], char='C') for k,s in opt.local_subproblems.items(): s._mpisppy_model.EF_Obj.deactivate() chached_ph_obj[k].activate()
def post_iter0(self): opt = self.opt # NOTE: the LShaped code negates the objective, so # we do the same here for consistency if 'cross_scen_options' in opt.options and \ 'valid_eta_bound' in opt.options['cross_scen_options']: valid_eta_bound = opt.options['cross_scen_options']['valid_eta_bound'] if not opt.is_minimizing: _eta_init = { k: -v for k,v in valid_eta_bound.items() } else: _eta_init = valid_eta_bound _eta_bounds = lambda m,k : (_eta_init[k], None) else: lb = (-sys.maxsize - 1) * 1. / len(opt.all_scenario_names) _eta_init = lambda m,k : lb _eta_bounds = lambda m,k : (lb, None) # eta is attached to each subproblem, regardless of bundles bundling = opt.bundling for k,s in opt.local_subproblems.items(): s._mpisppy_model.eta = pyo.Var(opt.all_scenario_names, initialize=_eta_init, bounds=_eta_bounds) if sputils.is_persistent(s._solver_plugin): for var in s._mpisppy_model.eta.values(): s._solver_plugin.add_var(var) if bundling: ## create a refence to eta on each subproblem for sn in s.scen_list: scenario = opt.local_scenarios[sn] scenario._mpisppy_model.eta = { k : s._mpisppy_model.eta[k] for k in opt.all_scenario_names } ## hold the PH object harmless self._disable_W_and_prox() for k,s in opt.local_subproblems.items(): obj = find_active_objective(s) repn = generate_standard_repn(obj.expr, quadratic=True) if len(repn.nonlinear_vars) > 0: raise ValueError("CrossScenario does not support models with nonlinear objective functions") if bundling: ## NOTE: this is slighly wasteful, in that for a bundle ## the first-stage cost appears len(s.scen_list) times ## If this really made a difference, we could use s.ref_vars ## to do the substitution nonant_vardata_list = list() for sn in s.scen_list: nonant_vardata_list.extend( \ opt.local_scenarios[sn]._PySPnode_list[0].nonant_vardata_list) else: nonant_vardata_list = s._PySPnode_list[0].nonant_vardata_list nonant_ids = set((id(var) for var in nonant_vardata_list)) linear_coefs = list(repn.linear_coefs) linear_vars = list(repn.linear_vars) quadratic_coefs = list(repn.quadratic_coefs) # adjust coefficients by scenario/bundle probability scen_prob = s.PySP_prob for i,var in enumerate(repn.linear_vars): if id(var) not in nonant_ids: linear_coefs[i] *= scen_prob for i,(x,y) in enumerate(repn.quadratic_vars): # only multiply through once if id(x) not in nonant_ids: quadratic_coefs[i] *= scen_prob elif id(y) not in nonant_ids: quadratic_coefs[i] *= scen_prob # NOTE: the LShaped code negates the objective, so # we do the same here for consistency if not opt.is_minimizing: for i,coef in enumerate(linear_coefs): linear_coefs[i] = -coef for i,coef in enumerate(quadratic_coefs): quadratic_coefs[i] = -coef # add the other etas if bundling: these_scenarios = set(s.scen_list) else: these_scenarios = [k] eta_scenarios = list() for sn in opt.all_scenario_names: if sn not in these_scenarios: linear_coefs.append(1) linear_vars.append(s._mpisppy_model.eta[sn]) eta_scenarios.append(sn) expr = LinearExpression(constant=repn.constant, linear_coefs=linear_coefs, linear_vars=linear_vars) if repn.quadratic_vars: expr += pyo.quicksum( (coef*x*y for coef,(x,y) in zip(quadratic_coefs, repn.quadratic_vars)) ) s._mpisppy_model.EF_obj = pyo.Expression(expr=expr) if opt.is_minimizing: s._mpisppy_model.EF_Obj = pyo.Objective(expr=s._mpisppy_model.EF_obj, sense=pyo.minimize) else: s._mpisppy_model.EF_Obj = pyo.Objective(expr=-s._mpisppy_model.EF_obj, sense=pyo.maximize) s._mpisppy_model.EF_Obj.deactivate() # add cut constraint dicts s._mpisppy_model.benders_cuts = pyo.Constraint(pyo.Any) s._mpisppy_model.inner_bound_constr = pyo.Constraint(pyo.Any) self._enable_W_and_prox() # try to get the initial eta LB cuts # (may not be available) opt.spcomm.get_from_cross_cuts()
def FormEF(self, scen_dict, EF_name=None): """ Make the EF for a list of scenarios. This function is mainly to build bundles. To build (and solve) the EF of the entire problem, use the EF class instead. Args: scen_dict (dict): Subset of local_scenarios; the scenarios to put in the EF. THe dictionary maps sccneario names (strings) to scenarios (Pyomo concrete model objects). EF_name (string, optional): Name for the resulting EF model. Returns: :class:`pyomo.environ.ConcreteModel`: The EF with explicit non-anticipativity constraints. Raises: RuntimeError: If the `scen_dict` is empty, or one of the scenarios in `scen_dict` is not owned locally (i.e. is not in `local_scenarios`). Note: We attach a list of the scenario names called _PySP_subsecen_names Note: We deactivate the objective on the scenarios. Note: The scenarios are sub-blocks, so they naturally get the EF solution Also the EF objective references Vars and Parms on the scenarios and hence is automatically updated when the scenario objectives are. THIS IS ALL CRITICAL to bundles. xxxx TBD: ask JP about objective function transmittal to persistent solvers Note: Objectives are scaled (normalized) by _mpisppy_probability """ if len(scen_dict) == 0: raise RuntimeError("Empty scenario list for EF") if len(scen_dict) == 1: sname, scenario_instance = list(scen_dict.items())[0] if EF_name is not None: print("WARNING: EF_name=" + EF_name + " not used; singleton=" + sname) print( "MAJOR WARNING: a bundle of size one encountered; if you try to compute bounds it might crash (Feb 2019)" ) return scenario_instance # The individual scenario instances are sub-blocks of the binding # instance. Needed to facilitate bundles + persistent solvers if not hasattr(self, "saved_objs"): # First bundle self.saved_objs = dict() for sname, scenario_instance in scen_dict.items(): if sname not in self.local_scenarios: raise RuntimeError("EF scen not in local_scenarios=" + sname) self.saved_objs[sname] = sputils.find_active_objective( scenario_instance) EF_instance = sputils._create_EF_from_scen_dict( scen_dict, EF_name=EF_name, nonant_for_fixed_vars=False) return EF_instance
def attach_PH_to_objective(self, add_duals, add_prox): """ Attach dual weight and prox terms to the objective function of the models in `local_scenarios`. Args: add_duals (boolean): If True, adds dual weight (Ws) to the objective. add_prox (boolean): If True, adds the prox term to the objective. """ if ('linearize_binary_proximal_terms' in self.options): lin_bin_prox = self.options['linearize_binary_proximal_terms'] else: lin_bin_prox = False if ('linearize_proximal_terms' in self.options): self._prox_approx = self.options['linearize_proximal_terms'] if 'proximal_linearization_tolerance' in self.options: self.prox_approx_tol = self.options[ 'proximal_linearization_tolerance'] else: self.prox_approx_tol = 1.e-1 if 'initial_proximal_cut_count' in self.options: initial_prox_cuts = self.options['initial_proximal_cut_count'] else: initial_prox_cuts = 2 else: self._prox_approx = False for (sname, scenario) in self.local_scenarios.items(): """Attach the dual and prox terms to the objective. """ if ((not add_duals) and (not add_prox)): return objfct = sputils.find_active_objective(scenario) is_min_problem = objfct.is_minimizing() xbars = scenario._mpisppy_model.xbars if self._prox_approx: # set-up pyomo IndexVar, but keep it sparse # since some nonants might be binary # Define the first cut to be _xsqvar >= 0 scenario._mpisppy_model.xsqvar = pyo.Var( scenario._mpisppy_data.nonant_indices, dense=False, within=pyo.NonNegativeReals) scenario._mpisppy_model.xsqvar_cuts = pyo.Constraint( scenario._mpisppy_data.nonant_indices, pyo.Integers) scenario._mpisppy_data.xsqvar_prox_approx = {} else: scenario._mpisppy_model.xsqvar = None scenario._mpisppy_data.xsqvar_prox_approx = False ph_term = 0 # Dual term (weights W) if (add_duals): scenario._mpisppy_model.WExpr = pyo.Expression(expr=\ sum(scenario._mpisppy_model.W[ndn_i] * xvar \ for ndn_i, xvar in scenario._mpisppy_data.nonant_indices.items()) ) ph_term += scenario._mpisppy_model.W_on * scenario._mpisppy_model.WExpr # Prox term (quadratic) if (add_prox): prox_expr = 0. for ndn_i, xvar in scenario._mpisppy_data.nonant_indices.items( ): # expand (x - xbar)**2 to (x**2 - 2*xbar*x + xbar**2) # x**2 is the only qradratic term, which might be # dealt with differently depending on user-set options if xvar.is_binary() and (lin_bin_prox or self._prox_approx): xvarsqrd = xvar elif self._prox_approx: xvarsqrd = scenario._mpisppy_model.xsqvar[ndn_i] scenario._mpisppy_data.xsqvar_prox_approx[ndn_i] = \ ProxApproxManager(xvar, xvarsqrd, scenario._mpisppy_model.xsqvar_cuts, ndn_i, initial_prox_cuts) else: xvarsqrd = xvar**2 prox_expr += (scenario._mpisppy_model.rho[ndn_i] / 2.0) * \ (xvarsqrd - 2.0 * xbars[ndn_i] * xvar + xbars[ndn_i]**2) scenario._mpisppy_model.ProxExpr = pyo.Expression( expr=prox_expr) ph_term += scenario._mpisppy_model.prox_on * scenario._mpisppy_model.ProxExpr if (is_min_problem): objfct.expr += ph_term else: objfct.expr -= ph_term
def APH_main(self, spcomm=None, finalize=True): """Execute the APH algorithm. Args: spcomm (SPCommunitator object): for intra or inter communications finalize (bool, optional, default=True): If True, call self.post_loops(), if False, do not, and return None for Eobj Returns: conv, Eobj, trivial_bound: The first two CANNOT BE EASILY INTERPRETED. Eobj is the expected, weighted objective with proximal term. It is not directly useful. The trivial bound is computed after iter 0 NOTE: You need an xhat finder either in denoument or in an extension. """ # Prep needs to be before iter 0 for bundling # (It could be split up) self.PH_Prep(attach_duals=False, attach_prox=False) # Begin APH-specific Prep for sname, scenario in self.local_scenarios.items(): # ys is plural of y scenario._mpisppy_model.y = pyo.Param( scenario._mpisppy_data.nonant_indices.keys(), initialize=0.0, mutable=True) scenario._mpisppy_model.ybars = pyo.Param( scenario._mpisppy_data.nonant_indices.keys(), initialize=0.0, mutable=True) scenario._mpisppy_model.z = pyo.Param( scenario._mpisppy_data.nonant_indices.keys(), initialize=0.0, mutable=True) # lag: we will support lagging back only to the last solve # IMPORTANT: pyomo does not support a second reference so no: # scenario._mpisppy_model.z_foropt = scenario._mpisppy_model.z if self.use_lag: scenario._mpisppy_model.z_foropt = pyo.Param( scenario._mpisppy_data.nonant_indices.keys(), initialize=0.0, mutable=True) scenario._mpisppy_model.W_foropt = pyo.Param( scenario._mpisppy_data.nonant_indices.keys(), initialize=0.0, mutable=True) objfct = find_active_objective(scenario) if self.use_lag: for (ndn, i), xvar in scenario._mpisppy_data.nonant_indices.items(): # proximal term objfct.expr += scenario._mpisppy_model.prox_on * \ (scenario._mpisppy_model.rho[(ndn,i)] /2.0) * \ (xvar**2 - 2.0*xvar*scenario._mpisppy_model.z_foropt[(ndn,i)] + scenario._mpisppy_model.z_foropt[(ndn,i)]**2) # W term objfct.expr += scenario._mpisppy_model.W_on * scenario._mpisppy_model.W_foropt[ ndn, i] * xvar else: for (ndn, i), xvar in scenario._mpisppy_data.nonant_indices.items(): # proximal term objfct.expr += scenario._mpisppy_model.prox_on * \ (scenario._mpisppy_model.rho[(ndn,i)] /2.0) * \ (xvar**2 - 2.0*xvar*scenario._mpisppy_model.z[(ndn,i)] + scenario._mpisppy_model.z[(ndn,i)]**2) # W term objfct.expr += scenario._mpisppy_model.W_on * scenario._mpisppy_model.W[ ndn, i] * xvar # End APH-specific Prep self.subproblem_creation(self.options["verbose"]) trivial_bound = self.Iter0() self.setup_Lens() self.setup_dispatchrecord() sleep_secs = self.options["async_sleep_secs"] lkwargs = None # nothing beyond synchro listener_gigs = { "FirstReduce": (self.listener_side_gig, lkwargs), "SecondReduce": None } self.synchronizer = listener_util.Synchronizer( comms=self.comms, Lens=self.Lens, work_fct=self.APH_iterk, rank=self.cylinder_rank, sleep_secs=sleep_secs, asynch=True, listener_gigs=listener_gigs) args = [spcomm] if spcomm is not None else [fullcomm] kwargs = None # {"extensions": extensions} self.synchronizer.run(args, kwargs) if finalize: Eobj = self.post_loops() else: Eobj = None # print(f"Debug: here's the dispatch record for rank={self.global_rank}") # for k,v in self.dispatchrecord.items(): # print(k, v) # print() # print("End dispatch record") return self.conv, Eobj, trivial_bound
def create_subproblem(self, scenario_name): """ the subproblem creation function passed into the BendersCutsGenerator """ instance = self.local_scenarios[scenario_name] nonant_list, nonant_ids = _get_nonant_ids(instance) # NOTE: since we use generate_standard_repn below, we need # to unfix any nonants so they'll properly appear # in the objective fixed_nonants = [ var for var in nonant_list if var.fixed ] for var in fixed_nonants: var.fixed = False # pulls the scenario objective expression, removes the first stage variables, and sets the new objective obj = find_active_objective(instance) if not hasattr(instance, "_mpisppy_probability"): instance._mpisppy_probability = 1. / self.scenario_count _mpisppy_probability = instance._mpisppy_probability repn = generate_standard_repn(obj.expr, quadratic=True) if len(repn.nonlinear_vars) > 0: raise ValueError("LShaped does not support models with nonlinear objective functions") linear_vars = list() linear_coefs = list() quadratic_vars = list() quadratic_coefs = list() ## we'll assume the constant is part of stage 1 (wlog it is), just ## like the first-stage bits of the objective constant = repn.constant ## only keep the second stage variables in the objective for coef, var in zip(repn.linear_coefs, repn.linear_vars): id_var = id(var) if id_var not in nonant_ids: linear_vars.append(var) linear_coefs.append(_mpisppy_probability*coef) for coef, (x,y) in zip(repn.quadratic_coefs, repn.quadratic_vars): id_x = id(x) id_y = id(y) if id_x not in nonant_ids or id_y not in nonant_ids: quadratic_coefs.append(_mpisppy_probability*coef) quadratic_vars.append((x,y)) # checks if model sense is max, if so negates the objective if not self.is_minimizing: for i,coef in enumerate(linear_coefs): linear_coefs[i] = -coef for i,coef in enumerate(quadratic_coefs): quadratic_coefs[i] = -coef expr = LinearExpression(constant=constant, linear_coefs=linear_coefs, linear_vars=linear_vars) if quadratic_coefs: expr += pyo.quicksum( (coef*x*y for coef,(x,y) in zip(quadratic_coefs, quadratic_vars)) ) instance.del_component(obj) # set subproblem objective function instance.obj = pyo.Objective(expr=expr, sense=pyo.minimize) ## need to do this here for validity if computing the eta bound if self.relax_root: # relaxes any integrality constraints for the subproblem RelaxIntegerVars().apply_to(instance) if self.compute_eta_bound: for var in fixed_nonants: var.fixed = True opt = pyo.SolverFactory(self.options["sp_solver"]) if self.options["sp_solver_options"]: for k,v in self.options["sp_solver_options"].items(): opt.options[k] = v if sputils.is_persistent(opt): set_instance_retry(instance, opt, scenario_name) res = opt.solve(tee=False) else: res = opt.solve(instance, tee=False) eta_lb = res.Problem[0].Lower_bound self.valid_eta_lb[scenario_name] = eta_lb # if not done above if not self.relax_root: # relaxes any integrality constraints for the subproblem RelaxIntegerVars().apply_to(instance) # iterates through constraints and removes first stage constraints from the model # the id dict is used to improve the speed of identifying the stage each variables belongs to for constr_data in list(itertools.chain( instance.component_data_objects(SOSConstraint, active=True, descend_into=True) , instance.component_data_objects(Constraint, active=True, descend_into=True))): if _first_stage_only(constr_data, nonant_ids): _del_con(constr_data) # creates the sub map to remove first stage variables from objective expression complicating_vars_map = pyo.ComponentMap() subproblem_to_root_vars_map = pyo.ComponentMap() # creates the complicating var map that connects the first stage variables in the sub problem to those in # the root problem -- also set the bounds on the subproblem root vars to be none for better cuts for var, rvar in zip(nonant_list, self.root_vars): if var.name not in rvar.name: # rvar.name may be part of a bundle raise Exception("Error: Complicating variable mismatch, sub-problem variables changed order") complicating_vars_map[rvar] = var subproblem_to_root_vars_map[var] = rvar # these are already enforced in the root # don't need to be enfored in the subproblems var.setlb(None) var.setub(None) var.fixed = False # this is for interefacing with PH code instance._mpisppy_model.subproblem_to_root_vars_map = subproblem_to_root_vars_map if self.store_subproblems: self.subproblems[scenario_name] = instance return instance, complicating_vars_map
def _create_root_with_scenarios(self): ef_scenarios = self.root_scenarios ## we want the correct probabilities to be set when ## calling create_EF if len(ef_scenarios) > 1: def scenario_creator_wrapper(name, **creator_options): scenario = self.scenario_creator(name, **creator_options) if not hasattr(scenario, '_mpisppy_probability'): scenario._mpisppy_probability = 1./len(self.all_scenario_names) return scenario root = sputils.create_EF( ef_scenarios, scenario_creator_wrapper, scenario_creator_kwargs=self.scenario_creator_kwargs, ) nonant_list, nonant_ids = _get_nonant_ids_EF(root) else: root = self.scenario_creator( ef_scenarios[0], **self.scenario_creator_kwargs, ) if not hasattr(root, '_mpisppy_probability'): root._mpisppy_probability = 1./len(self.all_scenario_names) nonant_list, nonant_ids = _get_nonant_ids(root) self.root_vars = nonant_list # creates the eta variables for scenarios that are NOT selected to be # included in the root problem eta_indx = [scenario_name for scenario_name in self.all_scenario_names if scenario_name not in self.root_scenarios] self._add_root_etas(root, eta_indx) obj = find_active_objective(root) repn = generate_standard_repn(obj.expr, quadratic=True) if len(repn.nonlinear_vars) > 0: raise ValueError("LShaped does not support models with nonlinear objective functions") linear_vars = list(repn.linear_vars) linear_coefs = list(repn.linear_coefs) quadratic_coefs = list(repn.quadratic_coefs) # adjust coefficients by scenario/bundle probability scen_prob = root._mpisppy_probability for i,var in enumerate(repn.linear_vars): if id(var) not in nonant_ids: linear_coefs[i] *= scen_prob for i,(x,y) in enumerate(repn.quadratic_vars): # only multiply through once if id(x) not in nonant_ids: quadratic_coefs[i] *= scen_prob elif id(y) not in nonant_ids: quadratic_coefs[i] *= scen_prob # NOTE: the LShaped code negates the objective, so # we do the same here for consistency if not self.is_minimizing: for i,coef in enumerate(linear_coefs): linear_coefs[i] = -coef for i,coef in enumerate(quadratic_coefs): quadratic_coefs[i] = -coef # add the etas for var in root.eta.values(): linear_vars.append(var) linear_coefs.append(1) expr = LinearExpression(constant=repn.constant, linear_coefs=linear_coefs, linear_vars=linear_vars) if repn.quadratic_vars: expr += pyo.quicksum( (coef*x*y for coef,(x,y) in zip(quadratic_coefs, repn.quadratic_vars)) ) root.del_component(obj) # set root objective function root.obj = pyo.Objective(expr=expr, sense=pyo.minimize) self.root = root
def _create_root_no_scenarios(self): # using the first scenario as a basis root = self.scenario_creator( self.all_scenario_names[0], **self.scenario_creator_kwargs ) if self.relax_root: RelaxIntegerVars().apply_to(root) nonant_list, nonant_ids = _get_nonant_ids(root) self.root_vars = nonant_list for constr_data in list(itertools.chain( root.component_data_objects(SOSConstraint, active=True, descend_into=True) , root.component_data_objects(Constraint, active=True, descend_into=True))): if not _first_stage_only(constr_data, nonant_ids): _del_con(constr_data) # delete the second stage variables for var in list(root.component_data_objects(Var, active=True, descend_into=True)): if id(var) not in nonant_ids: _del_var(var) self._add_root_etas(root, self.all_scenario_names) # pulls the current objective expression, adds in the eta variables, # and removes the second stage variables from the expression obj = find_active_objective(root) repn = generate_standard_repn(obj.expr, quadratic=True) if len(repn.nonlinear_vars) > 0: raise ValueError("LShaped does not support models with nonlinear objective functions") linear_vars = list() linear_coefs = list() quadratic_vars = list() quadratic_coefs = list() ## we'll assume the constant is part of stage 1 (wlog it is), just ## like the first-stage bits of the objective constant = repn.constant ## only keep the first stage variables in the objective for coef, var in zip(repn.linear_coefs, repn.linear_vars): id_var = id(var) if id_var in nonant_ids: linear_vars.append(var) linear_coefs.append(coef) for coef, (x,y) in zip(repn.quadratic_coefs, repn.quadratic_vars): id_x = id(x) id_y = id(y) if id_x in nonant_ids and id_y in nonant_ids: quadratic_coefs.append(coef) quadratic_vars.append((x,y)) # checks if model sense is max, if so negates the objective if not self.is_minimizing: for i,coef in enumerate(linear_coefs): linear_coefs[i] = -coef for i,coef in enumerate(quadratic_coefs): quadratic_coefs[i] = -coef # add the etas for var in root.eta.values(): linear_vars.append(var) linear_coefs.append(1) expr = LinearExpression(constant=constant, linear_coefs=linear_coefs, linear_vars=linear_vars) if quadratic_coefs: expr += pyo.quicksum( (coef*x*y for coef,(x,y) in zip(quadratic_coefs, quadratic_vars)) ) root.del_component(obj) # set root objective function root.obj = pyo.Objective(expr=expr, sense=pyo.minimize) self.root = root
def SDM(self, model_name): ''' Algorithm 2 in Boland et al. (with small tweaks) ''' mip = self.local_subproblems[model_name] qp = self.local_QP_subproblems[model_name] # Set the QP dual weights to the correct values If we are bundling, we # initialize the QP dual weights to be the dual weights associated with # the first scenario in the bundle (this should be okay, because each # scenario in the bundle has the same dual weights, analytically--maybe # a numerical problem). arb_scen_mip = self.local_scenarios[mip.scen_list[0]] \ if self.bundling else mip for (node_name, ix) in arb_scen_mip._mpisppy_data.nonant_indices: qp._mpisppy_model.W[node_name, ix]._value = \ arb_scen_mip._mpisppy_model.W[node_name, ix].value alpha = self.FW_options['FW_weight'] # Algorithm 3 line 6 xt = {ndn_i: (1 - alpha) * pyo.value(arb_scen_mip._mpisppy_model.xbars[ndn_i]) + alpha * pyo.value(xvar) for ndn_i, xvar in arb_scen_mip._mpisppy_data.nonant_indices.items() } for itr in range(self.FW_options['FW_iter_limit']): # Algorithm 2 line 4 mip_source = mip.scen_list if self.bundling else [model_name] for scenario_name in mip_source: scen_mip = self.local_scenarios[scenario_name] for ndn_i, nonant in scen_mip._mpisppy_data.nonant_indices.items(): x_source = xt[ndn_i] if itr==0 \ else nonant._value scen_mip._mpisppy_model.W[ndn_i]._value = ( qp._mpisppy_model.W[ndn_i]._value + scen_mip._mpisppy_model.rho[ndn_i]._value * (x_source - scen_mip._mpisppy_model.xbars[ndn_i]._value)) # Algorithm 2 line 5 if (sputils.is_persistent(mip._solver_plugin)): mip_obj = find_active_objective(mip) mip._solver_plugin.set_objective(mip_obj) mip_results = mip._solver_plugin.solve(mip) self._check_solve(mip_results, model_name + ' (MIP)') # Algorithm 2 lines 6--8 obj = find_active_objective(mip) if (itr == 0): if (self.is_minimizing): dual_bound = mip_results.Problem[0].Lower_bound else: dual_bound = mip_results.Problem[0].Upper_bound # Algorithm 2 line 9 (compute \Gamma^t) val0 = pyo.value(obj) new = replace_expressions(obj.expr, mip._mpisppy_data.mip_to_qp) val1 = pyo.value(new) obj.expr = replace_expressions(new, qp._mpisppy_data.qp_to_mip) if abs(val0) > 1e-9: stop_check = (val1 - val0) / abs(val0) # \Gamma^t in Boland, but normalized else: stop_check = val1 - val0 # \Gamma^t in Boland stop_check_tol = self.FW_options["stop_check_tol"]\ if "stop_check_tol" in self.FW_options else 1e-4 if (self.is_minimizing and stop_check < -stop_check_tol): print('Warning (fwph): convergence quantity Gamma^t = ' '{sc:.2e} (should be non-negative)'.format(sc=stop_check)) print('Try decreasing the MIP gap tolerance and re-solving') elif (not self.is_minimizing and stop_check > stop_check_tol): print('Warning (fwph): convergence quantity Gamma^t = ' '{sc:.2e} (should be non-positive)'.format(sc=stop_check)) print('Try decreasing the MIP gap tolerance and re-solving') self._add_QP_column(model_name) if (sputils.is_persistent(qp._QP_solver_plugin)): qp_obj = find_active_objective(qp) qp._QP_solver_plugin.set_objective(qp_obj) qp_results = qp._QP_solver_plugin.solve(qp) self._check_solve(qp_results, model_name + ' (QP)') if (stop_check < self.FW_options['FW_conv_thresh']): break # Re-set the mip._mpisppy_model.W so that the QP objective # is correct in the next major iteration mip_source = mip.scen_list if self.bundling else [model_name] for scenario_name in mip_source: scen_mip = self.local_scenarios[scenario_name] for (node_name, ix) in scen_mip._mpisppy_data.nonant_indices: scen_mip._mpisppy_model.W[node_name, ix]._value = \ qp._mpisppy_model.W[node_name, ix]._value return dual_bound