Package: serendipy
This is a package of Bi-level optimization designed forgurobiHow to InstallpipinstallserendipyHow to UseIt can automatically write the KKT condition of the lower problem, for example:min    f(x, y) 
s.t    g(x) >= 0 
       y = argmin  h(x, y) 
              s.t  u(x,y) >= 0KKT condition is composed withComplementary slackness,Primal feasibility,Dual feasibility,andStationarity condition, more  information aboutKarush–Kuhn–Tuckerconditions can be found athttps://en.wikipedia.org/wiki/Karush%E2%80%93Kuhn%E2%80%93Tucker_conditions.we can get the first three conditions of lower problem simply as :fromserendipyimportComplementary_great,Complementary_equal,MyExpr,tonp,to_value# build modelmodel=gurobi.Model()Dual_expression=[]Dual_objective=[]# build lower objective and variableslower_objective=xxxall_lower_level_vars=[xxx]# build lower constraintsexpr=u(x,y)dual_var=Complementary_great(expr,model,Dual_expression,Dual_objective,'name')Note: the key function isComplementary_great, its similar pair isComplementary_equal,Complementary_greatand its pair both have same signature. TakeComplementary_greatfor example, the first parameter isgurobi.Expr, which denotes the Primal constraint, and the expression is expected to be greater than zero(Complementary_equalexpect its expr to be equal to zero). The second parametergurobi.Modeldenotes the model which theexprbelongs to. The third parameterDual_expressionis a list, which collects the Lagrange term constraint by constraint. The forth parameterDual_objectiveis a list, which collects the dual object constraint by constraint. The last parameter is the name of the constraint. After add all the constraints,Dual_expressioncontains all the constraint-related Lagrange terms, andDual_objectivecontains all the dual objective terms.Note: InComplementary_great, theComplementary slacknessis handled by the big M method. Specifically,A E = 0is linearized asA <= M * B 
E <= M * (1-B)where B is a binary variable.after add all the constraints, we can get theStationarity conditionthrough "automatic derivation"my_expr=MyExpr(Dual_expression+lower_objective)forvarinall_lower_level_vars:expr=my_expr.getCoeff(var)my_expr.addConstr(expr,model)As the snippet shows, we first construct the Lagrange expression asmy_expr, then for all the lower level variables, we get the derivation throughmy_expr.getCoeff(var)asexpr, then we addexpr = 0tomodelas the last line.Now, we have got all the KKT conditions of lower problem, combined with the upper constraints and objective, we can solve the whole program !A complete exampleNow let look at a complete example, the original example comes fromhttps://static1.squarespace.com/static/5492d7f4e4b00040889988bd/t/57c06dfdd482e91c235c418b/1472229009395/9_PyomoBilevel.pdfpage 6:defexample():model=gurobi.Model()x=model.addVar(name='x')y=model.addVar(lb=-1*INF,ub=INF,name='y')lower_obj=y+0DE=[]Dobj=[]expr1=-3+x+yexpr2=0+2*x-yexpr3=12-2*x-yexpr4=-4+3*x-2*yexpr5=yComplementary_great(expr1,model,DE,Dobj,'e1')Complementary_great(expr2,model,DE,Dobj,'e2')Complementary_great(expr3,model,DE,Dobj,'e3')Complementary_great(expr4,model,DE,Dobj,'e4')Complementary_great(expr5,model,DE,Dobj,'e5')model.update()lagrange=lower_obj+sum(DE)myExpr=MyExpr(lagrange)forvarin[y]:exp=myExpr.getCoeff(var)myExpr.addConstr(exp,model,'[Lagrange]'+var.VarName)# model.setParam("IntegralityFocus", 1)model.setObjective(x-4*y)model.optimize()It has same result with the code written byyalmip, Cool🐒!Utility functionsBeside the KKT-related function, this package also provide three utility function astonp,to_value,to_value_np, because the variables got frommodel.addVars()don't support slice asnumpy, sotonpcan convert a value ofgurobi.tuple_dicttonumpy.narrayto support such slice, andto_valuesupport convert a variable with typegurobi.tuple_dictto its value which is  stored asnumpy.narray,to_value_npcan convert a variable with typegurobi.narrayto its value which is stored asnumpy.narray.Let's look at an example.In[1]:importgurobipyasgurobi
In[2]:model=gurobi.Model()In[3]:var33=model.addVars(3,3)In[4]:var33[:,1]---------------------------------------------------------------------------
TypeErrorTraceback(mostrecentcalllast)<ipython-input-4-87bcf4f6f185>in<module>
---->1var33[:,1]TypeError:unhashabletype:'slice'In order to maintain compatibility withnumpy, we can rewrite the code as :In[1]:model=gurobi.Model()In[2]:var33=tonp(model.addVars(3,3))In[3]:var33[:,1]Out[3]:array([<gurobi.Var*AwaitingModelUpdate*>,<gurobi.Var*AwaitingModelUpdate*>,<gurobi.Var*AwaitingModelUpdate*>],dtype=object)That all.Maybe you will be curious about the name of this packageserendipy, because I just write this package for one of my XUE MEIserendipity👧 LOL!  God bless her to be admitted to her ideal university.