# [Xk, Uk, phi, status, matrices] = linearmpc(model, constraint, penalty, terminal, [matrices])## Solves the following optimization problem:## N-1# min sum [ 1/2(x_k' Q x_k + u_k' R u_k ) ] + 1/2(x_N' P x_n)# u_k k=1## s.t x_k+1 = A x_k + B u_k# G x_k + H u_k <= psi# A_f x_N <= b_f## Linear time-invariant MPC.## Input 'model' has the following fields:# - A: A matrix for system.# - B: B matrix for system.# - N: horizon length in number of stemps.# - x0: initial state of system.## Input 'constraint' has the following fields:# - G: G matrix for constraints.# - H: G matrix for constraints.# - psi: psi vector for constraints.# Either all or none must be specified.## Input 'penalty' has the following fields:# - Q: Penalty matricies for state.# - R: Penalty matrix for input.# - P: Terminal penalty matrix.## Input terminal can be a vector of length x to specify a terminal equality# constraint, or a struct with fields A and b (corresponding to A_f and b_f in# the optimization given above).## The final output matrices is a struct with fields H, f, Alt, blt, Aeq, beq,# lb, and ub that define the QP formulation. If these are supplied in a struct# as a fifth argument, only the initial condition model.x0 is updated, and the# matrices are passed directly to the QP solver. This can save a lot of time if# you're solving the same problem over and over but with different initial# conditions. Note that minimal error checking is done# Check arguments.importnumpyasnpfromscipyimportsparsefromscipy.optimizeimportminimizeimportwarningsdeflinearmpc(model,constraint,penalty,terminal,matrices=None):""" Solves linear time-invariant MPC optimization problem. Args: model: dict with fields A, B, N, x0 constraint: dict with fields G, H, psi penalty: dict with fields Q, R, P terminal: vector or dict with fields A, b matrices: optional precomputed matrices Returns: Xk: optimal state trajectory Uk: optimal input trajectory phi: optimal cost status: solver status matrices: QP matrices """# Check argumentsifnot(4<=len([model,constraint,penalty,terminal,matrices])<=5):raiseValueError("Wrong number of arguments")# Build matrices if not suppliedifmatricesisNone:matrices=buildmatrices(model,constraint,penalty,terminal)# Get sizesnumx=model.B.shape[0]numu=model.B.shape[1]N=model.NnumVar=N*(numx+numu)+numx# Update initial conditionmatrices['lb'][0:numx]=model.x0matrices['ub'][0:numx]=model.x0# Get vectors to extract x and uallinds=np.mod(np.arange(numVar),numx+numu)xinds=allinds<numxuinds=allinds>=numx# Setup QP problemH=0.5*(matrices['H']+matrices['H'].T)# Make exactly symmetricdefobjective(x):return0.5*x.T@H@x+matrices['f'].T@xdefconstraint_eq(x):returnmatrices['Aeq']@x-matrices['beq']defconstraint_ineq(x):returnmatrices['blt']-matrices['Alt']@xbounds=list(zip(matrices['lb'],matrices['ub']))# Initial guessx0=np.zeros(numVar)# Solve QPwithwarnings.catch_warnings():warnings.simplefilter("ignore")result=minimize(objective,x0,method='SLSQP',bounds=bounds,constraints=[{'type':'eq','fun':constraint_eq},{'type':'ineq','fun':constraint_ineq}],options={'disp':False})# Process resultsstatus={'solver':'SLSQP','flag':result.status}status['optimal']=result.successifstatus['optimal']:X=result.xphi=result.funelse:X=np.nan*np.ones(numVar)phi=np.nan# Extract x and u trajectoriesXk=X[xinds].reshape((numx,N+1),order='F')Uk=X[uinds].reshape((numu,N),order='F')returnXk,Uk,phi,status,matricesdefbuildmatrices(model,constraint,penalty,terminal):"""Helper function to build QP matrices"""N=model.NA=model.AifA.shape[0]!=A.shape[1]:raiseValueError('model.A must be square')numx=A.shape[0]B=model.Bnumu=B.shape[1]ifhasattr(constraint,'G'):G=constraint.GH=constraint.Hpsi=constraint.psielse:G=np.zeros((0,numx))H=np.zeros((0,numu))psi=np.zeros((0,1))Q=penalty.QR=penalty.RP=penalty.PlittleH=sparse.block_diag([sparse.csr_matrix(Q),sparse.csr_matrix(R)])bigH=sparse.kron(sparse.eye(N),littleH)bigH=sparse.block_diag([bigH,sparse.csr_matrix(P)])bigf=np.zeros(bigH.shape[0])littleAeq1=sparse.hstack([sparse.csr_matrix(A),sparse.csr_matrix(B)])littleAeq2=sparse.hstack([-sparse.eye(A.shape[0]),sparse.csr_matrix(B.shape)])bigAeq=(sparse.kron(sparse.eye(N+1),littleAeq1)+sparse.kron(sparse.diags(np.ones(N),1),littleAeq2)).tocsr()bigAeq=bigAeq[:-numx,:-numu]bigbeq=np.zeros(numx*N)littleAlt1=sparse.hstack([sparse.csr_matrix(G),sparse.csr_matrix(H)])littleAlt2=sparse.hstack([sparse.csr_matrix(G.shape),sparse.csr_matrix(G.shape)])bigAlt=(sparse.kron(sparse.eye(N+1),littleAlt1)+sparse.kron(sparse.diags(np.ones(N),1),littleAlt2)).tocsr()bigAlt=bigAlt[:-littleAlt1.shape[0],:-numu]bigblt=np.tile(psi,N)numVar=len(bigf)LB=-np.inf*np.ones(numVar)UB=np.inf*np.ones(numVar)ifisinstance(terminal,dict)andall(kinterminalforkin['A','b']):Af=terminal['A']bf=terminal['b']bigAlt=sparse.vstack([bigAlt,sparse.hstack([sparse.csr_matrix((Af.shape[0],numVar-numx)),sparse.csr_matrix(Af)])])bigblt=np.concatenate([bigblt,np.asarray(bf).flatten()])elifisinstance(terminal,np.ndarray)andlen(terminal)==numx:LB[-numx:]=terminalUB[-numx:]=terminalelifterminalisNone:passelse:raiseValueError('Invalid terminal constraint')matrices={'H':bigH,'f':bigf,'Aeq':bigAeq,'beq':bigbeq,'Alt':bigAlt,'blt':bigblt,'lb':LB,'ub':UB}returnmatrices
# Set computations for Ex. 2.6, 2.7, and 2.8.# [depends] functions/linearmpc.pyimportsysimportnumpyasnpfromcontrolimportdlqrfromtypesimportSimpleNamespacesys.path.append('functions/')fromfindXnimportfindXnfromlinearmpcimportlinearmpc# Define model, cost function, and bounds.A=np.array([[2.0,1.0],[0.0,2.0]])B=np.array([[1.0,0.0],[0.0,1.0]])N=3alpha=1e-5Q=alpha*np.eye(2)R=np.eye(2)# Bounds.xlb=np.array([-5.0,-np.inf])xub=np.array([5.0,np.inf])ulb=np.array([-1.0,-1.0])uub=np.array([1.0,1.0])# Find LQR.K,P,_=dlqr(A,B,Q,R)K=-K# Sign convention.defvertices2lines(p,xlims,ylims):""" Returns 2-column array of line segments corresponding to edges of the polygon with vertices in columns of p (2 x N, must be in proper order). Each line segment has 101 interpolation points, and there is a row of NaNs inserted between each line segment. """dp=np.diff(p,axis=1)n_edges=p.shape[1]-1xarr=np.linspace(xlims[0],xlims[1],101).reshape(-1,1)yarr=np.linspace(ylims[0],ylims[1],101).reshape(-1,1)blocks=[]foriinrange(n_edges):slope=dp[1,i]/dp[0,i]ifnp.isinf(slope)ornp.isnan(slope):x=p[0,i]*np.ones((101,1))y=yarrelse:x=xarry=slope*(xarr-p[0,i])+p[1,i]blocks.append(np.hstack((x,y)))blocks.append(np.array([[np.nan,np.nan]]))returnnp.vstack(blocks)# Run the computation for a few different cases.terminals=['termeq','lqr']Xn={}V={}Z={}fortinterminals:Xn[t],V[t],Z[t]=findXn(A,B,K,N,xlb,xub,ulb,uub,t,doplot=False)# Simulate MPC from the initial starting point.model=SimpleNamespace(A=A,B=B,N=N)constraint=SimpleNamespace(**Z['lqr'])penalty=SimpleNamespace(Q=Q,R=R,P=P)terminal=Xn['lqr'][0]# LQR terminal set.Nsim=25xsim=np.zeros((2,Nsim+1))xsim[:,0]=np.array([0.5,0.5])# Initial condition.usim=np.zeros((2,Nsim))mpcmats=Nonefortinrange(Nsim):model.x0=xsim[:,t]xk,uk,_,status,mpcmats=linearmpc(model,constraint,penalty,terminal,mpcmats)usim[:,t]=uk[:,0]xsim[:,t+1]=xk[:,1]# Now check and see where the finite-horizon control law is the same as the# infinite-horizon control law.x1g,x2g=np.meshgrid(np.linspace(-1.86,1.86,50),np.linspace(-0.97,0.97,50))x0=np.vstack([x1g.ravel(),x2g.ravel()])terminal=Xn['lqr'][0]X3=Xn['lqr'][-1]# Feasible set.okaypts=np.all(X3['A']@x0<=X3['b'].reshape(-1,1),axis=0)x0=x0[:,okaypts]Npts=x0.shape[1]ininterior=np.all(terminal['A']@x0<=terminal['b'].reshape(-1,1),axis=0)mpcmats=Noneforiinrange(Npts):ifnotininterior[i]:model.x0=x0[:,i]xk,uk,_,status,mpcmats=linearmpc(model,constraint,penalty,terminal,mpcmats)ifstatus['optimal']:ininterior[i]=np.all(terminal['A']@xk[:,-1]<terminal['b'].flatten()-1e-3)x0interior=x0[:,ininterior]# Save data files.lims=np.array([-10.0,10.0])pa=V['lqr'][0].T# X_f vertices for LQR (Nx2)palines=vertices2lines(V['lqr'][0],lims,lims)# X_f facetspb=V['lqr'][-1].T# X_N vertices for LQR (Nx2)pblines=vertices2lines(V['lqr'][-1],lims,lims)# X_N facetspc=V['termeq'][-1].T# X_N vertices for terminal equality (Nx2)xtable=np.column_stack((np.arange(Nsim+1),xsim.T))# (Nsim+1) x 3utable=np.column_stack((np.arange(Nsim),usim.T))# Nsim x 3pe=x0interior.T# points where finite-horizon = inf-horizon (Kx2)withopen('Xinf.dat','w')asf:f.write('# pa\n')np.savetxt(f,pa,fmt='%.6f')f.write('\n\n')f.write('# palines\n')np.savetxt(f,palines,fmt='%.6f')f.write('\n\n')f.write('# pb\n')np.savetxt(f,pb,fmt='%.6f')f.write('\n\n')f.write('# pblines\n')np.savetxt(f,pblines,fmt='%.6f')f.write('\n\n')f.write('# pc\n')np.savetxt(f,pc,fmt='%.6f')f.write('\n\n')f.write('# xtable\n')np.savetxt(f,xtable,fmt='%.6f')f.write('\n\n')f.write('# utable\n')np.savetxt(f,utable,fmt='%.6f')f.write('\n\n')f.write('# pe\n')np.savetxt(f,pe,fmt='%.6f')f.write('\n\n')