Variable bounds in MPC with GEKKO - python
I'm trying to implement a thermostat control using MPC and GEKKO.
The state variable (temperature) should be within a lower and upper pre-specified temp values , temp_low and temp_upper in the code below.
Both bound vary along the day: one value per hour.
The objective function is the cost-to-go of using the heating. The price also changes along the day, TOU below. T_external is the room's exterior temperature that plays a role in the differential equation.
How can implement this so that it optimizes?
This is my attempt:
from gekko import GEKKO
import numpy as np
m = GEKKO(remote=False)
m.time = np.linspace(0,23,24)
#initialize variables
T_external = [50.,50.,50.,50.,45.,45.,45.,60.,60.,63.,64.,45.,45.,50.,52.,53.,53.,54.,54.,53.,52.,51.,50.,45.]
temp_low = [55.,55.,55.,55.,55.,55.,55.,68.,68.,68.,68.,55.,55.,68.,68.,68.,68.,55.,55.,55.,55.,55.,55.,55.]
temp_upper = [75.,75.,75.,75.,75.,75.,75.,70.,70.,70.,70.,75.,75.,70.,70.,70.,70.,75.,75.,75.,75.,75.,75.,75.]
TOU = [0.05,0.05,0.05,0.05,0.05,0.05,0.05,200.,200.,200.,200.,200.,200.,200.,200.,200.,200.,200.,200.,200.,200.,0.05,0.05,0.05]
b = m.Param(value=1.)
k = m.Param(value=0.05)
T_e = m.Param(value=T_external)
u = m.MV(value=[0]*24, lb=[0.0]*24, ub=[1.]*24)
u.STATUS = 1 # allow optimizer to change
# Controlled Variable
T = m.SV(value=[60]*24, lb=temp_low, ub=temp_upper)
m.Equation(T.dt() == k*(T_e-T) + b*u)
m.Obj(np.dot(TOU,u))
m.options.IMODE = 6
m.solve(debug=True)
When I run this I get:
#error: Model Expression
*** Error in syntax of function string: Missing operator
Position: 4
0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0
?
Gekko needs the constaints as inequality expressions where the variable T is compared to the upper TH or lower TL values. If you have b=1., it leads to an infeasible solution because the heater isn't powerful enough to maintain the temperature within the upper and lower limits. I changed the value to b=10 to get a feasible solution.
from gekko import GEKKO
import numpy as np
m = GEKKO(remote=False)
m.time = np.linspace(0,23,24)
#initialize variables
T_external = [50.,50.,50.,50.,45.,45.,45.,60.,60.,63.,\
64.,45.,45.,50.,52.,53.,53.,54.,54.,\
53.,52.,51.,50.,45.]
temp_low = [55.,55.,55.,55.,55.,55.,55.,68.,68.,68.,68.,\
55.,55.,68.,68.,68.,68.,55.,55.,55.,55.,55.,55.,55.]
temp_upper = [75.,75.,75.,75.,75.,75.,75.,70.,70.,70.,70.,75.,\
75.,70.,70.,70.,70.,75.,75.,75.,75.,75.,75.,75.]
TOU_v = [0.05,0.05,0.05,0.05,0.05,0.05,0.05,200.,200.,200.,200.,\
200.,200.,200.,200.,200.,200.,200.,200.,200.,200.,0.05,\
0.05,0.05]
b = m.Param(value=10.)
k = m.Param(value=0.05)
T_e = m.Param(value=T_external)
TL = m.Param(value=temp_low)
TH = m.Param(value=temp_upper)
TOU = m.Param(value=TOU_v)
u = m.MV(lb=0, ub=1)
u.STATUS = 1 # allow optimizer to change
# Controlled Variable
T = m.SV(value=60)
m.Equations([T>=TL,T<=TH])
m.Equation(T.dt() == k*(T_e-T) + b*u)
m.Minimize(TOU*u)
m.options.IMODE = 6
m.solve(disp=True,debug=True)
A potentially better solution is to set up soft constraints by redefining the limits as an error. You can minimize the error to stay within the limits. Even if it can't stay within the limits, the optimizer will do the best it can to minimize the infeasibility. This also allows you to trade-off multiple objectives simultaneously such as between comfort and cost.
from gekko import GEKKO
import numpy as np
m = GEKKO(remote=False)
m.time = np.linspace(0,23,24)
#initialize variables
T_external = [50.,50.,50.,50.,45.,45.,45.,60.,60.,63.,\
64.,45.,45.,50.,52.,53.,53.,54.,54.,\
53.,52.,51.,50.,45.]
temp_low = [55.,55.,55.,55.,55.,55.,55.,68.,68.,68.,68.,\
55.,55.,68.,68.,68.,68.,55.,55.,55.,55.,55.,55.,55.]
temp_upper = [75.,75.,75.,75.,75.,75.,75.,70.,70.,70.,70.,75.,\
75.,70.,70.,70.,70.,75.,75.,75.,75.,75.,75.,75.]
TOU_v = [0.05,0.05,0.05,0.05,0.05,0.05,0.05,200.,200.,200.,200.,\
200.,200.,200.,200.,200.,200.,200.,200.,200.,200.,0.05,\
0.05,0.05]
b = m.Param(value=10.)
k = m.Param(value=0.05)
T_e = m.Param(value=T_external)
TL = m.Param(value=temp_low)
TH = m.Param(value=temp_upper)
TOU = m.Param(value=TOU_v)
u = m.MV(lb=0, ub=1)
u.STATUS = 1 # allow optimizer to change
# Controlled Variable
T = m.SV(value=60)
# Soft constraints
eH = m.CV(value=0)
eL = m.CV(value=0)
eH.SPHI=0; eH.WSPHI=100; eH.WSPLO=0 ; eH.STATUS = 1
eL.SPLO=0; eL.WSPHI=0 ; eL.WSPLO=100; eL.STATUS = 1
m.Equations([eH==T-TH,eL==T-TL])
m.Equation(T.dt() == k*(T_e-T) + b*u)
m.Minimize(TOU*u)
m.options.IMODE = 6
m.solve(disp=True,debug=True)
import matplotlib.pyplot as plt
plt.subplot(2,1,1)
plt.plot(m.time,temp_low,'k--')
plt.plot(m.time,temp_upper,'k--')
plt.plot(m.time,T.value,'r-')
plt.ylabel('Temperature')
plt.subplot(2,1,2)
plt.step(m.time,u.value,'b:')
plt.ylabel('Heater')
plt.xlabel('Time (hr)')
plt.show()
Related
GEKKO python, how to run a calculation until I get a desired value of a variable
Does anyone know how to run a dynamic simulation in GEKKO Python (IMODE 7) until a certain variable takes on the desired value? For example, I have a differential equation and I need to do the calculation until the pressure is 1 bar. I understand how to make a calculation for 100 seconds, 200 seconds, etc., but I do not understand how to let it run until I have a variable of a specific desired value. The code that I have is kind of large, so in principle this code that I want to modify: I want to have it without a while statement because every while statement creates a new model that takes time. And I have much bigger code and this time makes a negative impact. #Start calculation pressure_final = 0 t_guess = 10 while pressure_final < 1: m = GEKKO(remote = False) # Create GEKKO model k = 0.1 P = m.Var(0.1) #Time discretization tf = t_guess nt = int(tf/1) + 1 m.time = np.linspace(0,tf,nt) #Left boundary pressure m.Equation(P.dt() == k*P) print("Start Pressurization") m.options.IMODE = 7 m.solve(disp = False) print("Finished pressurization") t_guess = t_guess + 1 pressure_final = P[-1] I will be very grateful for the answer!
There are two ways to accomplish this. The first method is with step-by-step simulation with IMODE=4. There is no need to recreate the model each cycle. A solve command time shifts the final conditions to the initial conditions automatically because m.options.TIME_SHIFT=1. The pressure may exceed the final target value using this method. import numpy as np from gekko import GEKKO m = GEKKO(remote = False) # Create GEKKO model once k = 0.1 P = m.Var(0.1) m.time = [0,1] m.Equation(P.dt() == k*P) m.options.IMODE = 4 m.options.NODES = 3 t=[0]; Pr=[0.1] # store time / Pressure for i in range(100): print(i) m.solve(disp = False) t.append(i+1) Pr.append(P[-1]) if Pr[-1]>=1: break import matplotlib.pyplot as plt plt.plot(t,Pr,'b-o') plt.plot([0,t[-1]],[1,1],'r--') plt.ylabel('Pressure'), plt.xlabel('Time') plt.savefig('pressure.png',dpi=300) plt.show() The second method is to make the final time an optimization variable an FV with tf.STATUS=1. tf = m.FV(1,lb=1,ub=100) tf.STATUS=1 Divide all differential terms by the tf value and use m.time between 0 and 1. m.time = np.linspace(0,1,20) m.Equation(P.dt()/tf == k*P) Use IMODE=6 to minimize the deviation of the final pressure from 1 with final = np.zeros(20); final[-1]=1 f = m.Param(final) m.Minimize(f*(P-1)**2) This gives the exact time of Final Time: 23.041309678 when the pressure limit is reached. import numpy as np from gekko import GEKKO m = GEKKO(remote = False) # Create GEKKO model once k = 0.1 P = m.Var(0.1) m.time = np.linspace(0,1,20) tf = m.FV(1,lb=1,ub=100); tf.STATUS=1 m.Equation(P.dt()/tf == k*P) final = np.zeros(20); final[-1]=1 f = m.Param(final) m.Minimize(f*(P-1)**2) m.options.IMODE = 6 m.options.NODES = 3 m.solve(disp = False) print('Final Time: ', tf[-1]) import matplotlib.pyplot as plt plt.plot(m.time*tf[-1],P,'b-o') plt.plot([0,tf[-1]],[1,1],'r--') plt.ylabel('Pressure'), plt.xlabel('Time') plt.savefig('pressure.png',dpi=300) plt.show()
Gekko Optimization doesn't give me a unique answer although there is a unique answer for my model
my optimization model works but with different initial values for the main variable (Pre), it gives a different answer! not the optimal one! but this should have one answer. I do not understand why! try: from pip import main as pipmain except: from pip._internal import main as pipmain pipmain(['install','gekko']) from gekko import GEKKO import pandas as pd import numpy as np import matplotlib.pyplot as plt #Initialize Model m = GEKKO(remote=False) #define parameter df=pd.read_excel (r'C:\Users\....') Pw=pd.DataFrame(df).values eta = m.Const(value=0.6) Pre=m.Var(lb=20, ub=30) Pre.value=23 def f(Pw,Pre): Dplus=m.Var(value=0) Dminus=m.Var(value=0) for i in range(744): D=float(Pw[i])-Pre.value if D>=0: Dplus.value=Dplus.value+D*eta.value elif D<0: Dminus.value=Dminus.value+D return Dplus+Dminus #constraint: m.Equation(f(Pw,Pre)>=0) #Objective: m.Minimize(f(Pw,Pre)) #Set global options: m.options.IMODE = 2 #steady state optimization #Solve simulation: m.solve()
You shouldn't use .value to build model equations because it only references the initial guess value, not the variable value. Use the m.if3() (preferred) or m.if2() functions to use conditional statements in the model. Here is an example of m.if3(): import numpy as np import matplotlib.pyplot as plt from gekko import GEKKO m = GEKKO(remote=False) p = m.Param() y = m.if3(p-4,p**2,p+1) # solve with condition<0 p.value = 3 m.solve(disp=False) print(y.value) # solve with condition>=0 p.value = 5 m.solve(disp=False) print(y.value) An even better way to incorporate conditional statements is to use slack variables. It appears that your application is for energy storage where there is inefficiency in the storage process given by eta. Here is a simple energy storage problem (see problem 4) where the loss from storage and retrieval is embedded in the optimization. from gekko import GEKKO import numpy as np import matplotlib.pyplot as plt m = GEKKO(remote=False) m.time = np.linspace(0,1,101) g = m.FV(); g.STATUS = 1 # production s = m.Var(1e-2, lb=0) # storage inventory store = m.Var() # store energy rate s_in = m.Var(lb=0) # store slack variable recover = m.Var() # recover energy rate s_out = m.Var(lb=0) # recover slack variable eta = 0.7 d = m.Param(-2*np.sin(2*np.pi*m.time)+10) m.periodic(s) m.Equations([g + recover/eta - store >= d, g - d == s_out - s_in, store == g - d + s_in, recover == d - g + s_out, s.dt() == store - recover/eta, store * recover <= 0]) m.Minimize(g) m.options.SOLVER = 1 m.options.IMODE = 6 m.options.NODES = 3 m.solve() plt.figure(figsize=(6,3)) plt.subplot(2,1,1) plt.plot(m.time,d,'r-',label='Demand') plt.plot(m.time,g,'b:',label='Prod') plt.legend(); plt.grid(); plt.xlim([0,1]) plt.subplot(2,1,2) plt.plot(m.time,s,'k-',label='Storage') plt.plot(m.time,store,'g--', label='Store Rate') plt.plot(m.time,recover,'b:', label='Recover Rate') plt.legend(); plt.grid(); plt.xlim([0,1]) plt.show()
1D Plane flight optimal control problem in gekko
I need to solve 1D plane flight optimal control problem. I have a plane that is 1000m high. I need it to travel 1000m forward along x-axis while minimizing fuel consumption. And when it achieves x=1000 I need program to stop. My objective function looks like this: min J => -mass(tf). (By maximizing mass, the consumed fuel gets minimized). Optimizer controls the accelerator pedal. The problem is subject to the following constraints: dx/dt = V; dV/dt = (Throttle - Drag)/mass; dm/dt = -Throttle * fuelflow. I have developed a Gekko / Python script as follows. However, the optimization script does not achieve a solution, raising: Exception: #error: Solution Not Found I have tried to change stuff here and there, but it did not work and I'm not sure what the problem is. Here is my code: import numpy as np import matplotlib.pyplot as plt from gekko import GEKKO import math #Gekko model m = GEKKO(remote=False) #Time points nt = 101 m.time = np.linspace(0,100,nt) # Parameters Tcontr = m.MV(value=0.5,lb=0.3,ub=1) #throttle pedal position Tcontr.STATUS = 1 Tcontr.DCOST = 0 # Variables Ro = 1.1 #air density S = 122.6 Cd = value=0.1 FuelFlow = m.Var(value=0.7) D = m.Var() #drag Thrmax = 200000 #maximum theoretical throttle Thr = m.Var() #real throttle V = m.Var() #Velocity x = m.Var(value=0) mass = m.Var(value=60000) p = np.zeros(nt) p[-1] = 1.0 final = m.Param(value=p) # Equations m.Equation(x.dt()==V) m.Equation(Thr==Tcontr*Thrmax) #Throttle m.Equation(V.dt()==(Thr-D)/mass) m.Equation(mass.dt()==-Thr*FuelFlow) m.Equation(D==0.5*Ro*(V**2)*Cd*S) #Drag m.Equation(final*x==1000) # to stop when x==1000 is achieved # Objective Function m.Obj(-mass*final) m.options.IMODE = 6 m.options.NODES = 2 m.options.MV_TYPE = 1 m.options.SOLVER = 3 m.open_folder() m.solve()
It seems like the major problem was that the upper bound of your MV (Tcontr) is little bit too low to meet all the constraints. And, I guess the "FuelFlow" is need to be a m.Const as I assumed it is a fuel flowrate per throttle. Lastly, when you use the final for designating the value at the last time point, you need to make sure the associated Equation is also feasible for the entire time steps. I suggest to use this instead. m.Equation(final*(x-1000)==0) That way, you can achive your goal without posing any infeasible equation as your previous equation did. Your previous equation is infeasible at the most of the time points except the last time point with non-zero value. It's been resulting "0 (LHS) = 1000 (RHS)" before the final time point. Please see the modified code below. import numpy as np import matplotlib.pyplot as plt from gekko import GEKKO # import math #Gekko model m = GEKKO(remote=True) #Time points nt = 101 m.time = np.linspace(0,100,nt) # Parameters Tcontr = m.MV(value=0.5,lb=0.3,ub=10) #throttle pedal position Tcontr.STATUS = 1 Tcontr.DCOST = 1e-2 # Variables Ro = 1.1 #air density S = 122.6 Cd = value=0.1 FuelFlow = m.Const(value=0.7) D = m.Var() #drag Thrmax = 200000 #maximum theoretical throttle Thr = m.Var() #real throttle V = m.Var() #Velocity x = m.Var(value=0) mass = m.Var(value=60000) p = np.zeros(nt) p[-1] = 1.0 final = m.Param(value=p) # Equations m.Equation(x.dt()==V) m.Equation(Thr==Tcontr*Thrmax) #Throttle m.Equation(mass*V.dt()==(Thr-D)) m.Equation(mass.dt()==-Thr*FuelFlow) m.Equation(D==0.5*Ro*(V**2)*Cd*S) #Drag m.Equation(final*(x-1000)==0) # to stop when x==1000 is achieved # Objective Function m.Obj(-mass*final) m.options.IMODE = 6 m.options.NODES = 3 m.options.MV_TYPE = 1 m.options.SOLVER = 3 # m.open_folder() m.solve()
Gekko Example 1b get different result with different form of the same terminal constraints
I tried Example 1b of https://apmonitor.com/do/index.php/Main/DynamicOptimizationBenchmarks. I construct two different form of the same terminal conditions as follows: import numpy as np import matplotlib.pyplot as plt from gekko import GEKKO m = GEKKO() nt = 101 m.time = np.linspace(0,2,nt) # Variables x1 = m.Var(value=1) x2 = m.Var(value=0) u = m.Var(value=-0.48) p = np.zeros(nt) p[-1] = 1.0 final = m.Param(value=p) # Equations m.Equation(x1.dt()==u) m.Equation(x2.dt()==x1**2 + u**2) # terminal conditions m.Equation(final*(x1-1)==0) # 1st form # m.fix_final(x1,val=1) # 2nd form m.Obj(x2*final) m.options.IMODE = 6 m.solve() plt.figure(1) plt.plot(m.time,x1.value,'k:',linewidth=2,label=r'$x_1$') plt.plot(m.time,x2.value,'b-',linewidth=2,label=r'$x_2$') plt.plot(m.time,u.value,'r--',linewidth=2,label=r'$u$') plt.legend(loc='best')plt.xlabel('Time')plt.ylabel('Value')plt.show() They behave differently and achieved different solutions as follows. 1st form m.Equation(final*(x1-1)==0): 2nd form m.fix_final(x1,val=1): I prefer to use fix_final to define such a terminal constraint like x(tf)=0. But it seems to make the problem unsolvable. Like the 13rd problem in https://apmonitor.com/wiki/index.php/Main/GekkoPythonOptimization. from gekko import GEKKO import numpy as np import matplotlib.pyplot as plt m = GEKKO() # initialize GEKKO nt = 501 m.time = np.linspace(0,1,nt) x1 = m.Var(value=np.pi/2.0) x2 = m.Var(value=4.0) x3 = m.Var(value=0.0) p = np.zeros(nt) # final time = 1 p[-1] = 1.0 final = m.Param(value=p) tf = m.FV(value=1.0,lb=0.1,ub=100.0) tf.STATUS = 1 u = m.MV(value=0,lb=-2,ub=2) u.STATUS = 1 m.Equation(x1.dt()==u*tf) m.Equation(x2.dt()==m.cos(x1)*tf) m.Equation(x3.dt()==m.sin(x1)*tf) # terminal constraints m.Equation(x2*final<=0) # get solution m.Equation(x3*final<=0) # or # m.fix(x2, pos=len(m.time)-1,val=0) # solution not found # m.fix(x3, pos=len(m.time)-1,val=0) # or # m.fix_final(x2,val=0) # solution not found # m.fix_final(x3,val=0) m.Obj(tf) m.options.IMODE = 6 m.solve() plt.figure(1) plt.plot(tm,x1.value,'k-',label=r'$x_1$') plt.plot(tm,x2.value,'b-',label=r'$x_2$') plt.plot(tm,x3.value,'g--',label=r'$x_3$') plt.plot(tm,u.value,'r--',label=r'$u$')plt.legend(loc='best')plt.xlabel('Time')plt.show()
When you fix the final condition of a variable it also sets the derivative to zero. This is also discussed as an issue on GitHub. There are workarounds for this issue such as soft terminal constraints (see Trajectory Planner with GEKKO is not able to handle given goal velocities) or hard terminal constraints in the way that you showed.
Why is GEKKO not picking up the initial measurement?
In using GEKKO to model a dynamic system with an initial measurement, GEKKO seems to be ignoring the measurement completely even with FSTATUS turned on. What causes this and how can I get GEKKO to recognize the initial measurement? I would expect the solver to take the initial measurement into account an adjust the solution accordingly. from gekko import GEKKO import numpy as np import matplotlib.pyplot as plt # measurement tm = 0 xm = 25 m = GEKKO() m.time = np.linspace(0,20,41) tau = 10 b = m.Param(value=50) K = m.Param(value=0.8) # Manipulated Variable u = m.MV(value=0, lb=0, ub=100) u.STATUS = 1 # allow optimizer to change u.DCOST = 0.1 u.DMAX = 30 # Controlled Variable x = m.CV(value=0,name='x') x.STATUS = 1 # add the SP to the objective m.options.CV_TYPE = 2 # squared error x.SP = 40 # set point x.TR_INIT = 1 # set point trajectory x.TAU = 5 # time constant of trajectory x.FSTATUS = 1 x.MEAS = xm # Process model m.Equation(tau*x.dt() == -x + K*u) m.options.IMODE = 6 # control m.solve() # get additional solution information import json with open(m.path+'//results.json') as f: results = json.load(f) plt.figure() plt.subplot(2,1,1) plt.plot(m.time,u.value,'b-',label='MV Optimized') plt.legend() plt.ylabel('Input') plt.subplot(2,1,2) plt.plot(tm,xm,'ro', label='Measurement') plt.plot(m.time,results['x.tr'],'k-',label='Reference Trajectory') plt.plot(m.time,results['x.bcv'],'r--',label='CV Response') plt.ylabel('Output') plt.xlabel('Time') plt.legend() plt.show()
Gekko ignores the measurement on the first cycle for initialization of the MPC. If you do another solve then it uses the measurement. m.solve() # for MPC initialization x.MEAS = xm m.solve() # update initial condition with measurement The feedback status (FSTATUS) is a first-order filter for the measurements that ranges between 0 (no update) and 1 (full measurement update). MEAS = LSTVAL * (1-FSTATUS) + MEAS * FSTATUS The new measurement (MEAS) is then used in the bias calculation. There is an unbiased (raw prediction not affected by measurements) model prediction and a biased model prediction. The bias is calculated as the difference between the unbiased model prediction and the measurement. BIAS = MEAS - UNBIASED_MODEL from gekko import GEKKO import numpy as np import matplotlib.pyplot as plt # measurement tm = 0 xm = 25 m = GEKKO() m.time = np.linspace(0,20,41) tau = 10 b = m.Param(value=50) K = m.Param(value=0.8) # Manipulated Variable u = m.MV(value=0, lb=0, ub=100) u.STATUS = 1 # allow optimizer to change u.DCOST = 0.1 u.DMAX = 30 # Controlled Variable x = m.CV(value=0,name='x') x.STATUS = 1 # add the SP to the objective m.options.CV_TYPE = 2 # squared error x.SP = 40 # set point x.TR_INIT = 1 # set point trajectory x.TAU = 5 # time constant of trajectory x.FSTATUS = 1 # Process model m.Equation(tau*x.dt() == -x + K*u) m.options.IMODE = 6 # control m.solve(disp=False) m.options.TIME_SHIFT = 0 x.MEAS = xm m.solve(disp=False) # turn off time shift, only for initialization m.options.TIME_SHIFT = 1 # get additional solution information import json with open(m.path+'//results.json') as f: results = json.load(f) plt.figure() plt.subplot(2,1,1) plt.plot(m.time,u.value,'b-',label='MV Optimized') plt.legend() plt.ylabel('Input') plt.ylim([-5,105]) plt.subplot(2,1,2) plt.plot(tm,xm,'ro', label='Measurement') plt.plot(m.time,results['x.tr'],'k-',label='Reference Trajectory') plt.plot(m.time,results['x.bcv'],'r--',label='CV Response Biased') plt.plot(m.time,x.value,'g:',label='CV Response Unbiased') plt.ylim([-1,41]) plt.ylabel('Output') plt.xlabel('Time') plt.legend() plt.show() This is how it currently works because there are no LSTVAL or unbiased model predictions for the calculations mentioned above. The first cycle calculates those values and allows updating on subsequent cycles. If you do need the updated values on the first cycle then you can solve with option m.option.TIME_SHIFT=0 on the second solve to not update the initial conditions of your model. You will want to change TIME_SHIFT=1 for subsequent cycles to have the expected time-progression of the dynamic model.