def make_cuts(self, coefs): # take the coefficient array and assemble cuts accordingly # this should have already been set in the extension ! opt = self.opt # rows are # [ const, eta_coeff, *nonant_coeffs ] row_len = 1 + 1 + self.nonant_len outer_iter = int(coefs[-1]) bundling = opt.bundling if opt.bundling: for bn, b in opt.local_subproblems.items(): persistent_solver = sputils.is_persistent(b._solver_plugin) ## get an arbitrary scenario s = opt.local_scenarios[b.scen_list[0]] for idx, k in enumerate(opt.all_scenario_names): row = coefs[row_len * idx:row_len * (idx + 1)] # the row could be all zeros, # which doesn't do anything if (row == 0.).all(): continue # rows are # [ const, eta_coeff, *nonant_coeffs ] linear_const = row[0] linear_coefs = list(row[1:]) linear_vars = [b.eta[k]] for ndn_i in s._nonant_indexes: ## for bundles, we add the constrains only ## to the reference first stage variables linear_vars.append(b.ref_vars[ndn_i]) cut_expr = LinearExpression(constant=linear_const, linear_coefs=linear_coefs, linear_vars=linear_vars) b._benders_cuts[outer_iter, k] = (None, cut_expr, 0) if persistent_solver: b._solver_plugin.add_constraint( b._benders_cuts[outer_iter, k]) else: for sn, s in opt.local_subproblems.items(): persistent_solver = sputils.is_persistent(s._solver_plugin) for idx, k in enumerate(opt.all_scenario_names): row = coefs[row_len * idx:row_len * (idx + 1)] # the row could be all zeros, # which doesn't do anything if (row == 0.).all(): continue # rows are # [ const, eta_coeff, *nonant_coeffs ] linear_const = row[0] linear_coefs = list(row[1:]) linear_vars = [s.eta[k]] linear_vars.extend(s._nonant_indexes.values()) cut_expr = LinearExpression(constant=linear_const, linear_coefs=linear_coefs, linear_vars=linear_vars) s._benders_cuts[outer_iter, k] = (None, cut_expr, 0.) if persistent_solver: s._solver_plugin.add_constraint( s._benders_cuts[outer_iter, k]) # NOTE: the LShaped code negates the objective, so # we do the same here for consistency ib = self.BestInnerBound ob = self.BestOuterBound if not opt.is_minimizing: ib = -ib ob = -ob add_cut = (isfinite(ib) or isfinite(ob)) and \ ((ib < self.best_inner_bound) or (ob > self.best_outer_bound)) if add_cut: self.best_inner_bound = ib self.best_outer_bound = ob for sn, s in opt.local_subproblems.items(): persistent_solver = sputils.is_persistent(s._solver_plugin) prior_outer_iter = list(s._ib_constr.keys()) s._ib_constr[outer_iter] = (ob, s._EF_obj, ib) if persistent_solver: s._solver_plugin.add_constraint(s._ib_constr[outer_iter]) # remove other ib constraints (we only need the tightest) for it in prior_outer_iter: if persistent_solver: s._solver_plugin.remove_constraint(s._ib_constr[it]) del s._ib_constr[it] ## helping the extention track cuts self.new_cuts = True
def _build_sum_expression(variables, constant=0.): expr = LinearExpression() expr.linear_vars = [item[1] for item in variables] expr.linear_coefs = [item[0] for item in variables] expr.constant = constant return expr
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 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]._mpisppy_node_list[0].nonant_vardata_list) else: nonant_vardata_list = s._mpisppy_node_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._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 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 _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 _create_EF_from_scen_dict(scen_dict, EF_name=None, nonant_for_fixed_vars=True): """ Create a ConcreteModel of the extensive form from a scenario dictionary. Args: scen_dict (dict): Dictionary whose keys are scenario names and values are ConcreteModel objects corresponding to each scenario. EF_name (str--optional): Name of the resulting EF model. nonant_for_fixed_vars (bool--optional): If True, enforces non-anticipativity constraints for all variables, including those which have been fixed. Deafult is True. Returns: EF_instance (ConcreteModel): ConcreteModel of extensive form with explicity non-anticipativity constraints. Notes: The non-anticipativity constraints are enforced by creating "reference variables" at each node in the scenario tree (excluding leaves) and enforcing that all the variables for each scenario at that node are equal to the reference variables. This function is called directly when creating bundles for PH. Does NOT assume that each scenario is equally likely. Raises an AttributeError if a scenario object is encountered which does not have a .PySP_prob attribute. Added the flag nonant_for_fixed_vars because original code only enforced non-anticipativity for non-fixed vars, which is not always desirable in the context of bundling. This allows for more fine-grained control. """ is_min, clear = _models_have_same_sense(scen_dict) if (not clear): raise RuntimeError('Cannot build the extensive form out of models ' 'with different objective senses') sense = pyo.minimize if is_min else pyo.maximize EF_instance = pyo.ConcreteModel(name=EF_name) EF_instance.EF_Obj = pyo.Objective(expr=0.0, sense=sense) EF_instance._PySP_feas_indicator = None EF_instance._PySP_subscen_names = [] EF_instance.PySP_prob = 0 for (sname, scenario_instance) in scen_dict.items(): EF_instance.add_component(sname, scenario_instance) EF_instance._PySP_subscen_names.append(sname) # Now deactivate the scenario instance Objective scenario_objs = get_objs(scenario_instance) for obj_func in scenario_objs: obj_func.deactivate() obj_func = scenario_objs[0] # Select the first objective try: EF_instance.EF_Obj.expr += scenario_instance.PySP_prob * obj_func.expr EF_instance.PySP_prob += scenario_instance.PySP_prob except AttributeError as e: raise AttributeError( "Scenario " + sname + " has no specified " "probability. Specify a value for the attribute " " PySP_prob and try again.") from e # Normalization does nothing when solving the full EF, but is required for # appropraite scaling of EFs used as bundles. EF_instance.EF_Obj.expr /= EF_instance.PySP_prob # For each node in the scenario tree, we need to collect the # nonanticipative vars and create the constraints for them, # which we do using a reference variable. ref_vars = dict() # keys are _nonant_indexes (i.e. a node name and a # variable number) ref_suppl_vars = dict() EF_instance._PySP_nlens = dict() nonant_constr = pyo.Constraint(pyo.Any, name='_C_EF_') EF_instance.add_component('_C_EF_', nonant_constr) nonant_constr_suppl = pyo.Constraint(pyo.Any, name='_C_EF_suppl') EF_instance.add_component('_C_EF_suppl', nonant_constr_suppl) for (sname, s) in scen_dict.items(): if (not hasattr(s, '_PySP_nlens')): nlens = { node.name: len(node.nonant_vardata_list) for node in s._PySPnode_list } else: nlens = s._PySP_nlens for (node_name, num_nonant_vars) in nlens.items(): # copy nlens to EF if (node_name in EF_instance._PySP_nlens.keys() and num_nonant_vars != EF_instance._PySP_nlens[node_name]): raise RuntimeError("Number of non-anticipative variables is " "not consistent at node " + node_name + " in scenario " + sname) EF_instance._PySP_nlens[node_name] = num_nonant_vars nlens_ef_suppl = { node.name: len(node.nonant_ef_suppl_vardata_list) for node in s._PySPnode_list } for node in s._PySPnode_list: ndn = node.name for i in range(nlens[ndn]): v = node.nonant_vardata_list[i] if (ndn, i) not in ref_vars: # create the reference variable as a singleton with long name # xxxx maybe index by _nonant_index ???? rather than singleton VAR ??? ref_vars[(ndn, i)] = v # Add a non-anticipativity constraint, except in the case when # the variable is fixed and nonant_for_fixed_vars=False. else: if (nonant_for_fixed_vars): expr = LinearExpression( linear_coefs=[1, -1], linear_vars=[v, ref_vars[(ndn, i)]], constant=0.) nonant_constr[(ndn, i, sname)] = (expr, 0.0) elif (not v.is_fixed()): expr = LinearExpression( linear_coefs=[1, -1], linear_vars=[v, ref_vars[(ndn, i)]], constant=0.) nonant_constr[(ndn, i, sname)] = (expr, 0.0) for i in range(nlens_ef_suppl[ndn]): v = node.nonant_ef_suppl_vardata_list[i] if (ndn, i) not in ref_suppl_vars: # create the reference variable as a singleton with long name # xxxx maybe index by _nonant_index ???? rather than singleton VAR ??? ref_suppl_vars[(ndn, i)] = v # Add a non-anticipativity constraint, expect in the case when # the variable is fixed and nonant_for_fixed_vars=False. else: if (nonant_for_fixed_vars): expr = LinearExpression( linear_coefs=[1, -1], linear_vars=[v, ref_suppl_vars[(ndn, i)]], constant=0.) nonant_constr_suppl[(ndn, i, sname)] = (expr, 0.0) elif (not v.is_fixed()): expr = LinearExpression( linear_coefs=[1, -1], linear_vars=[v, ref_suppl_vars[(ndn, i)]], constant=0.) nonant_constr_suppl[(ndn, i, sname)] = (expr, 0.0) EF_instance.ref_vars = ref_vars EF_instance.ref_suppl_vars = ref_suppl_vars return EF_instance
def neg_load_generate_mismatch_tolerance_rule(m): linear_vars = list(m.negLoadGenerateMismatch.values()) linear_coefs = [1.] * len(linear_vars) return (0., LinearExpression(linear_vars=linear_vars, linear_coefs=linear_coefs), None)
def declare_ineq_p_branch_thermal_lbub( model, index_set, branches, p_thermal_limits, approximation_type=ApproximationType.BTHETA, slacks=False): """ Create the inequality constraints for the branch thermal limits based on the power variables or expressions. """ m = model con_set = decl.declare_set('_con_ineq_p_branch_thermal_lbub', model=model, index_set=index_set) # flag for if slacks are on the model if slacks: if not hasattr(model, 'pf_slack_pos'): raise Exception( 'No positive slack branch variables on model, but slacks=True') if not hasattr(model, 'pf_slack_neg'): raise Exception( 'No negative slack branch variables on model, but slacks=True') m.ineq_pf_branch_thermal_lb = pe.Constraint(con_set) m.ineq_pf_branch_thermal_ub = pe.Constraint(con_set) if approximation_type == ApproximationType.BTHETA or \ approximation_type == ApproximationType.PTDF: for branch_name in con_set: if p_thermal_limits[branch_name] is None: continue if slacks and branch_name in m.pf_slack_neg: pf_bn = m.pf[branch_name] if hasattr(pf_bn, 'expr') and isinstance( pf_bn.expr, LinearExpression): ## create a copy old_expr = pf_bn.expr expr = LinearExpression( constant=old_expr.constant, linear_vars=old_expr.linear_vars[:] + [m.pf_slack_neg[branch_name]], linear_coefs=old_expr.linear_coefs[:] + [1], ) else: expr = m.pf[branch_name] + m.pf_slack_neg[branch_name] m.ineq_pf_branch_thermal_lb[branch_name] = \ (-p_thermal_limits[branch_name], expr, None) else: m.ineq_pf_branch_thermal_lb[branch_name] = \ (-p_thermal_limits[branch_name], m.pf[branch_name], None) if slacks and branch_name in m.pf_slack_pos: pf_bn = m.pf[branch_name] if hasattr(pf_bn, 'expr') and isinstance( pf_bn.expr, LinearExpression): ## create a copy old_expr = pf_bn.expr expr = LinearExpression( constant=old_expr.constant, linear_vars=old_expr.linear_vars[:] + [m.pf_slack_pos[branch_name]], linear_coefs=old_expr.linear_coefs[:] + [-1], ) else: expr = m.pf[branch_name] - m.pf_slack_pos[branch_name] m.ineq_pf_branch_thermal_lb[branch_name] = \ (None, expr, p_thermal_limits[branch_name]) else: m.ineq_pf_branch_thermal_ub[branch_name] = \ (None, m.pf[branch_name], p_thermal_limits[branch_name])