# Applies offset-free linear MPC to the nonlinear CSTR.# See Pannocchia and Rawlings, AIChE J, 2002.importnumpyasnpimportmpctoolsasmpcfromtypesimportSimpleNamespaceimportmatplotlib.pyplotaspltimportcontrolasctfrommiscimportgnuplotsaveimportos# Parameters and sizes for the nonlinear systemDelta=1Nx=3Nu=2Ny=NxNp=1small=1e-5# small number# Parameterspars=SimpleNamespace()pars.T0=350# Kpars.c0=1# kmol/m^3pars.r=0.219# mpars.k0=7.2e10# min^-1pars.E=8750# Kpars.U=54.94# kJ/(min m^2 K)pars.rho=1e3# kg/m^3pars.Cp=0.239# kJ / (kg K)pars.DeltaH=-5e4# kJ/kmolpars.A=np.pi*pars.r**2# m^2pars.rhoCp=pars.rho*pars.Cp# kJ / (m^3 K)defcstrode(x,u,p,pars):""" Nonlinear ODE model for a reactor. """c,T,h=xh+=np.finfo(float).eps# Avoid division by zeroTc,F=uF0=p[0]k=pars.k0*np.exp(-pars.E/T)rate=k*cdcdt=F0*(pars.c0-c)/(pars.A*h)-ratedTdt=(F0*(pars.T0-T)/(pars.A*h)-pars.DeltaH/pars.rhoCp*rate+2*pars.U/(pars.r*pars.rhoCp)*(Tc-T))dhdt=(F0-F)/pars.Areturnnp.array([dcdt,dTdt,dhdt])ode_casadi=mpc.getCasadiFunc(lambdax,u,p:cstrode(x,u,p,pars),[Nx,Nu,Np],['x','u','p'],'ode')cstrsim=mpc.getCasadiIntegrator(lambdax,u,p:cstrode(x,u,p,pars),Delta,[Nx,Nu,Np],['x','u','p'],'cstr')# Steady-state valuescs=0.8778# kmol/m^3Ts=324.5# Khs=0.659# mFs=0.1# m^3/minTcs=300# KF0s=0.1# m^3/minxs=np.array([cs,Ts,hs])us=np.array([Tcs,Fs])ps=np.array([F0s])CVs=[0,2]# control concentration and height# Simulate a few steps so we actually get dx/dt = 0 at steady-stateforiinrange(10):xs=cstrsim(xs,us,ps).toarray().flatten()cs=xs[0]Ts=xs[1]hs=xs[2]# Get linearized model and linear controllermodel=mpc.getLinearizedModel(ode_casadi,[xs,us,ps],['A','B','Bp'],Delta)# measure only T and hA=model['A']B=model['B']C=np.eye(Ny)Bp=model['Bp']Q=np.diag(1/(xs**2))R=np.diag(1/(us**2))K=-ct.dlqr(A,B,Q,R)[0]# Pick whether to use good distrubance modeldisturbancemodels=['Good','Offset','Undetectable','No']data={}fordmodelindisturbancemodels:print('Choosing disturbance model:',dmodel)ifdmodel=='Good':# distrubance model; no offsetNd=3Bd=np.zeros((Nx,Nd))Bd[:,2]=B[:,1]Cd=np.array([[1,0,0],[0,0,0],[0,1,0]])elifdmodel=='Offset':# disturbance model with offsetNd=2Bd=np.zeros((Nx,Nd))Cd=np.array([[1,0],[0,0],[0,1]])elifdmodel=='Undetectable':Nd=3Bd=np.zeros((Nx,Nd))Cd=np.eye(Ny,Nd)elifdmodel=='No':Nd=0Bd=np.zeros((Nx,Nd))Cd=np.zeros((Ny,Nd))else:raiseValueError('Unknown disturbance model')Aaug=np.block([[A,Bd],[np.zeros((Nd,Nx)),np.eye(Nd)]])Baug=np.vstack([B,np.zeros((Nd,Nu))])Caug=np.hstack([C,Cd])Naug=Aaug.shape[0]# Detectability test of disturbance modeldetect_matrix=np.vstack([np.eye(Naug)-Aaug,Caug])detec=np.linalg.matrix_rank(detect_matrix)ifdetec<Nx+Nd:print('***Augmented system is not detectable! \n')break# Set up state estimator; use Kalman filterQw=np.zeros((Naug,Naug))Qw[:Nx,:Nx]=small*np.eye(Nx)Qw[Nx:,Nx:]=small*np.eye(Nd)Qw[-1,-1]=1.0Rv=small*np.diag(xs**2)Daug=np.eye(Naug)L,Pe,_=ct.dlqe(Aaug,Daug,Caug,Qw,Rv)L=np.linalg.inv(Aaug)@L# convert combined gain to corrector gainLx=L[:Nx,:]Ld=L[Nx:,:]# Closed-loop simulationnp.random.seed(123)Nsim=50x=np.full((Nx,Nsim+1),np.nan)x[:,0]=0.0y=np.full((Ny,Nsim+1),np.nan)u=np.full((Nu,Nsim),np.nan)v=np.zeros((Ny,Nsim+1))xhatm=x.copy()dhatm=np.full((Nd,Nsim+1),np.nan)dhatm[:,0]=0.0xhat=np.full((Nx,Nsim),np.nan)dhat=np.full((Nd,Nsim),np.nan)xtarg=np.zeros((Nx,Nsim))utarg=np.zeros((Nu,Nsim))# Distrubance and setpointp=np.zeros((Np,Nsim))p[:,9:]=0.1*F0sysp=np.zeros((Ny,Nsim))# Steady-state target matricesiflen(CVs)>Nu:raiseValueError('At most 2 CVs can be specified!')H=np.eye(Ny)H=H[CVs,:]Ginv=np.linalg.solve(np.block([[np.eye(Nx)-A,-B],[H@C,np.zeros((H.shape[0],Nu))]]),np.eye(Nx+Nu))# start loopforiinrange(Nsim):# Take measurementy[:,i]=C@x[:,i]+v[:,i]# Advance state measuremente=y[:,i]-C@xhatm[:,i]-Cd@dhatm[:,i]xhat[:,i]=xhatm[:,i]+Lx@edhat[:,i]=dhatm[:,i]+Ld@e# use steady-state target selectortarg=Ginv@np.hstack([Bd@dhat[:,i],H@(ysp[:,i]-Cd@dhat[:,i])])xtarg[:,i]=targ[:Nx]utarg[:,i]=targ[Nx:]# Apply control lawu[:,i]=K@(xhat[:,i]-xtarg[:,i])+utarg[:,i]# Evolve plant. Out variables are deviation but cstrsim needs positionalx[:,i+1]=cstrsim(x[:,i]+xs,u[:,i]+us,p[:,i]+ps).toarray().flatten()-xs# Advance state estimatexhatm[:,i+1]=A@xhat[:,i]+B@u[:,i]+Bd@dhat[:,i]dhatm[:,i+1]=dhat[:,i]# Convert to positional unitsu=u+us[:,np.newaxis]x=x+xs[:,np.newaxis]ysp=ysp+C@xs[:,np.newaxis]ysp[[iforiinrange(Ny)ifinotinCVs],:]=np.nan# Mask out uncontrolled variablesysp=np.hstack([ysp,ysp[:,-1:]])# Duplicate final point# Make a plott=np.arange(Nsim+1)*Deltaifnotos.getenv('OMIT_PLOTS')=='true':mpc.plots.mpcplot(x,u,t,xsp=ysp,xnames=['c','T','h'],unames=['T_c','F'],timefirst=False)plt.show()data[dmodel]={}data[dmodel]['x']=np.vstack([t,x]).Tu=np.hstack([u,u[:,-2:-1]])# Duplicate final pointdata[dmodel]['u']=np.vstack([t,u]).Tgnuplotsave('cstr.dat',data)