Search code examples
python-3.xgekko

Why the delay parameter affects the prediction curve?


I put the result of Gekko's calculation into the queue, after a delay, write the first value of the queue to TCLab Arduino. I use this method to simulate a factory large time delay system, then I optimize Gekko parameters to achieve better control effect. When I add a delay=1 in the model, I got a pretty good prediction curve: enter image description here

The final control effect is also pretty good: enter image description here

But when I set delay=80, no other parameters are modified, the prediction curve is not ideal: enter image description here

The final control effect is also bad: enter image description here

Why the delay parameter affects the prediction curve? I think the reference trajectory should also shift with time delay. How could I solve this problem?

Here is the code:

import tclab
import numpy as np
import time
import matplotlib.pyplot as plt
from gekko import GEKKO
import _thread
import queue
import threading
import json

# Connect to Arduino
a = tclab.TCLabModel()

R = threading.Lock()

# Turn LED on
print('LED On')
a.LED(100)

# Simulate a time delay
delay = 80

# Run time in minutes
run_time = 60.0

# Number of cycles
loops = int(60.0*run_time)
tm = np.zeros(loops)

# Temperature (K)
T1 = np.ones(loops) * a.T1 # temperature (degC)
Tsp1 = np.ones(loops) * 40.0 # set point (degC)

# heater values
Q1s = np.ones(loops) * 0.0

#########################################################
# Initialize Model
#########################################################
# use remote=True for MacOS
m = GEKKO(name='tclab-mpc',remote=False)

# 240 second time horizon
m.time = np.linspace(0,240,121)

# Parameters
Q1_ss = m.Param(value=0)
TC1_ss = m.Param(value=a.T1)
Kp = m.Param(value=1.0)
tau = m.Param(value=120.0)

# Manipulated variable
Q1 = m.MV(value=Q1_ss.VALUE, name='q1')
Q1.STATUS = 1  # use to control temperature
Q1.FSTATUS = 0 # no feedback measurement
Q1.LOWER = 0.0
Q1.UPPER = 100.0
Q1.DMAX = 50
Q1.DCOST = 2.0

# Controlled variable
TC1 = m.CV(value=TC1_ss.VALUE, name='tc1')
TC1.STATUS = 1     # minimize error with setpoint range
TC1.FSTATUS = 1    # receive measurement
TC1.TR_INIT = 2    # reference trajectory
TC1.TAU = 30      # time constant for response

# 添加延时
Q1d=m.Var()
m.delay(Q1, Q1d, delay)
# Equation
m.Equation(tau * TC1.dt() + (TC1-TC1_ss) == Kp * Q1d)

# Steady-state initialization
m.options.IMODE = 1
m.solve()

# Global Options
m.options.IMODE   = 6 # MPC
m.options.CV_TYPE = 1 # Objective type
m.options.NODES   = 2 # Collocation nodes
m.options.SOLVER  = 1 # 1=APOPT, 3=IPOPT
##################################################################

# Create plot
plt.figure()
plt.ion()
plt.show()

filter_tc1 = []

q = queue.Queue()
flag = False
def setQ1():
    global flag
    global a
    last_time = time.time()
    while True:
        sleep_max = 2.0
        sleep = sleep_max - (time.time() - last_time)
        print("q.qsize()", q.qsize())
        if sleep >= 0.01:
            time.sleep(sleep)
        else:
            time.sleep(0.01)

        # Record time and change in time
        last_time = time.time()
        while q.qsize() >= delay:
            q1 = q.get()
            print(f'Q1: {q1} ')
            try:
                R.acquire()
                print("write Q1:", a.Q1(float(q1)))
                R.release()
            except:
                print("convertion error!")

_thread.start_new_thread(setQ1, ())

# Rolling average filtering
def movefilter(predata, new, n):
    if len(predata) < n:
        predata.append(new)
    else:
        predata.pop(0)
        predata.append(new)
    return np.average(predata)

# Main Loop
start_time = time.time()
prev_time = start_time
last_Q1 = 0
try:
    for i in range(1,loops):
        # Sleep time
        sleep_max = 2.0
        sleep = sleep_max - (time.time() - prev_time)
        print(time.time() - prev_time)
        if sleep>=0.01:
            time.sleep(sleep)
        else:
            time.sleep(0.01)

        # Record time and change in time
        t = time.time()
        dt = t - prev_time
        prev_time = t
        tm[i] = t - start_time

        # Read temperatures in Kelvin
        R.acquire()
        curr_T1 = a.T1
        R.release()
        last_T1 = curr_T1
        avg_T1 = movefilter(filter_tc1, last_T1, 3)
        T1[i] = curr_T1
        # T1[i] = a.T1
        # T2[i] = a.T2

        ###############################
        ### MPC CONTROLLER          ###
        ###############################
        TC1.MEAS = avg_T1
        # input setpoint with deadband +/- DT
        DT = 1.0
        TC1.SPHI = Tsp1[i] + DT
        TC1.SPLO = Tsp1[i] - DT
        try:
            # solve MPC
            m.solve(disp=False)
        except:
            pass
        # test for successful solution
        if (m.options.APPSTATUS==1):
            # retrieve the first Q value
            Q1s[i] = Q1.NEWVAL
            with open(m.path+'//results.json') as f:
                results = json.load(f)
        else:
            # not successful, set heater to zero
            Q1s[i] = 0

        # Write output (0-100)
        q.put(Q1s[i])

        # Plot
        plt.clf()
        ax=plt.subplot(2,1,1)
        ax.grid()
        plt.plot(tm[0:i],T1[0:i],'ro',markersize=3,label=r'$T_1$')
        plt.plot(tm[0:i],Tsp1[0:i],'b-',markersize=3,label=r'$T_1 Setpoint$')
        plt.plot(tm[i]+m.time,results['tc1.bcv'],'k-.',\
                 label=r'$T_1$ predicted',linewidth=3)
        plt.plot(tm[i]+m.time,results['tc1.tr_hi'],'k--',\
                 label=r'$T_1$ trajectory')
        plt.plot(tm[i]+m.time,results['tc1.tr_lo'],'k--')
        plt.ylabel('Temperature (degC)')
        plt.legend(loc='best')
        ax=plt.subplot(2,1,2)
        ax.grid()
        plt.plot(tm[0:i],Q1s[0:i],'r-',linewidth=3,label=r'$Q_1$')
        plt.plot(tm[i]+m.time,Q1.value,'k-.',\
                 label=r'$Q_1$ plan',linewidth=3)
        # plt.plot(tm[0:i],Q2s[0:i],'b:',LineWidth=3,label=r'$Q_2$')
        plt.ylabel('Heaters')
        plt.xlabel('Time (sec)')
        plt.legend(loc='best')
        plt.draw()
        plt.pause(0.05)

    # Turn off heaters
    a.Q1(0)
    a.Q2(0)
    print('Shutting down')
    a.close()

# Allow user to end loop with Ctrl-C
except KeyboardInterrupt:
    # Disconnect from Arduino
    a.Q1(0)
    a.Q2(0)
    print('Shutting down')
    a.close()

# Make sure serial connection still closes when there's an error
except:
    # Disconnect from Arduino
    a.Q1(0)
    a.Q2(0)
    print('Error: Shutting down')
    a.close()
    raise

Solution

  • The controller is working as intended. The problem is a well-known issue with model mismatch and delay. Classical control methods are to use a Smith Predictor to compensate for the delay. With MPC, the delay is built into the model, similar to the function of the Smith Predictor. If you change the model to:

    Kp = m.Param(value=0.7)
    tau = m.Param(value=180.0)
    

    then the performance is improved:

    improved control performance

    A modification to the reference trajectory also improves performance because the controller is focused on the long-term target versus the near-term error that it can't control.

    Method 1 - Open Reference Trajectory at Beginning

    TC1.TR_INIT = 2    # reference trajectory
    TC1.TR_OPEN = 20   # more focus on final destination
    TC1.TAU = 60       # time constant for response
    

    Method 2 - Start Penalizing Later

    Use CV_WGT_START to only penalize after a certain amount of time delay.

    TC1.TR_INIT = 0    # reference trajectory
    m.options.CV_WGT_START = 80
    

    The parameters need one more adjustment to better match the process.

    Kp = m.Param(value=0.8)
    tau = m.Param(value=160.0)
    

    improved control performance

    Method 3: Define Own Reference Trajectory

    If you do want a custom reference trajectory then you can define a new variable SP that is delayed as SPd. Define a new CV error that is the difference between TC1 and the delayed reference trajectory.

    SP = m.Var()
    m.Equation(30 * SP.dt() == -SP + 40)
    SPd = m.Var()
    m.delay(SP, SPd, delay)
    
    error = m.CV(value=0)
    m.Equation(error==SPd - TC1)
    error.STATUS = 1     # minimize error with setpoint range
    error.FSTATUS = 0    # receive measurement
    error.TR_INIT = 0    # reference trajectory
    error.SPHI    = 0.5  # drive error close to 0
    error.SPLO    = -0.5 # drive error close to 0
    

    With this approach, you'll need to work out a method to update the error measurement for measurement feedback.

    There are also many times that the MPC calculations slightly exceed the cycle time. This is creating a model mismatch that you can help by increasing the MPC cycle time to 3 seconds.

    Modified script

    import tclab
    import numpy as np
    import time
    import matplotlib.pyplot as plt
    from gekko import GEKKO
    import _thread
    import queue
    import threading
    import json
    
    # Connect to Arduino
    a = tclab.TCLabModel()
    
    R = threading.Lock()
    
    # Turn LED on
    print('LED On')
    a.LED(100)
    
    # Simulate a time delay
    delay = 80
    
    # Run time in minutes
    run_time = 60.0
    
    # Number of cycles
    loops = int(60.0*run_time)
    tm = np.zeros(loops)
    
    # Temperature (K)
    T1 = np.ones(loops) * a.T1 # temperature (degC)
    Tsp1 = np.ones(loops) * 40.0 # set point (degC)
    
    # heater values
    Q1s = np.ones(loops) * 0.0
    
    #########################################################
    # Initialize Model
    #########################################################
    # use remote=True for MacOS
    m = GEKKO(name='tclab-mpc',remote=False)
    
    # 240 second time horizon
    m.time = np.linspace(0,240,121)
    
    # Parameters
    Q1_ss = m.Param(value=0)
    TC1_ss = m.Param(value=a.T1)
    Kp = m.Param(value=0.7)
    tau = m.Param(value=180.0)
    
    # Manipulated variable
    Q1 = m.MV(value=Q1_ss.VALUE, name='q1')
    Q1.STATUS = 1  # use to control temperature
    Q1.FSTATUS = 0 # no feedback measurement
    Q1.LOWER = 0.0
    Q1.UPPER = 100.0
    Q1.DMAX = 50
    Q1.DCOST = 2.0
    
    # Controlled variable
    TC1 = m.CV(value=TC1_ss.VALUE, name='tc1')
    TC1.STATUS = 1     # minimize error with setpoint range
    TC1.FSTATUS = 1    # receive measurement
    TC1.TR_INIT = 0    # reference trajectory
    m.options.CV_WGT_START = 80
    
    # 添加延时
    Q1d=m.Var()
    m.delay(Q1, Q1d, delay)
    # Equation
    m.Equation(tau * TC1.dt() + (TC1-TC1_ss) == Kp * Q1d)
    
    # Steady-state initialization
    m.options.IMODE = 1
    m.solve()
    
    # Global Options
    m.options.IMODE   = 6 # MPC
    m.options.CV_TYPE = 1 # Objective type
    m.options.NODES   = 2 # Collocation nodes
    m.options.SOLVER  = 1 # 1=APOPT, 3=IPOPT
    ##################################################################
    
    # Create plot
    plt.figure()
    plt.ion()
    plt.show()
    
    filter_tc1 = []
    
    q = queue.Queue()
    flag = False
    def setQ1():
        global flag
        global a
        last_time = time.time()
        while True:
            sleep_max = 2.0
            sleep = sleep_max - (time.time() - last_time)
            print("q.qsize()", q.qsize())
            if sleep >= 0.01:
                time.sleep(sleep)
            else:
                time.sleep(0.01)
    
            # Record time and change in time
            last_time = time.time()
            while q.qsize() >= delay:
                q1 = q.get()
                print(f'Q1: {q1} ')
                try:
                    R.acquire()
                    print("write Q1:", a.Q1(float(q1)))
                    R.release()
                except:
                    print("convertion error!")
    
    _thread.start_new_thread(setQ1, ())
    
    # Rolling average filtering
    def movefilter(predata, new, n):
        if len(predata) < n:
            predata.append(new)
        else:
            predata.pop(0)
            predata.append(new)
        return np.average(predata)
    
    # Main Loop
    start_time = time.time()
    prev_time = start_time
    last_Q1 = 0
    try:
        for i in range(1,loops):
            # Sleep time
            sleep_max = 2.0
            sleep = sleep_max - (time.time() - prev_time)
            print(time.time() - prev_time)
            if sleep>=0.01:
                time.sleep(sleep)
            else:
                print('Warning: Dynamic mismatch from excess MPC time')
                print('         Consider increasing the cycle time to 3+ sec')
                time.sleep(0.01)
    
            # Record time and change in time
            t = time.time()
            dt = t - prev_time
            prev_time = t
            tm[i] = t - start_time
    
            # Read temperatures in Kelvin
            R.acquire()
            curr_T1 = a.T1
            R.release()
            last_T1 = curr_T1
            avg_T1 = movefilter(filter_tc1, last_T1, 3)
            T1[i] = curr_T1
            # T1[i] = a.T1
            # T2[i] = a.T2
    
            ###############################
            ### MPC CONTROLLER          ###
            ###############################
            TC1.MEAS = avg_T1
            # input setpoint with deadband +/- DT
            DT = 1.0
            TC1.SPHI = Tsp1[i] + DT
            TC1.SPLO = Tsp1[i] - DT
            try:
                # solve MPC
                m.solve(disp=False)
            except:
                pass
            # test for successful solution
            if (m.options.APPSTATUS==1):
                # retrieve the first Q value
                Q1s[i] = Q1.NEWVAL
                with open(m.path+'//results.json') as f:
                    results = json.load(f)
            else:
                # not successful, set heater to zero
                Q1s[i] = 0
    
            # Write output (0-100)
            q.put(Q1s[i])
    
            # Plot
            plt.clf()
            ax=plt.subplot(2,1,1)
            ax.grid()
            plt.plot(tm[0:i],T1[0:i],'ro',markersize=3,label=r'$T_1$')
            plt.plot(tm[0:i],Tsp1[0:i],'b-',markersize=3,label=r'$T_1 Setpoint$')
            plt.plot(tm[i]+m.time,results['tc1.bcv'],'k-.',\
                     label=r'$T_1$ predicted',linewidth=3)
            plt.plot(tm[i]+m.time,results['tc1.tr_hi'],'k--',\
                     label=r'$T_1$ trajectory')
            plt.plot(tm[i]+m.time,results['tc1.tr_lo'],'k--')
            plt.ylabel('Temperature (degC)')
            plt.legend(loc='best')
            ax=plt.subplot(2,1,2)
            ax.grid()
            plt.plot(tm[0:i],Q1s[0:i],'r-',linewidth=3,label=r'$Q_1$')
            plt.plot(tm[i]+m.time,Q1.value,'k-.',\
                     label=r'$Q_1$ plan',linewidth=3)
            # plt.plot(tm[0:i],Q2s[0:i],'b:',LineWidth=3,label=r'$Q_2$')
            plt.ylabel('Heaters')
            plt.xlabel('Time (sec)')
            plt.legend(loc='best')
            plt.draw()
            plt.pause(0.05)
    
        # Turn off heaters
        a.Q1(0)
        a.Q2(0)
        print('Shutting down')
        a.close()
    
    # Allow user to end loop with Ctrl-C
    except KeyboardInterrupt:
        # Disconnect from Arduino
        a.Q1(0)
        a.Q2(0)
        print('Shutting down')
        a.close()
    
    # Make sure serial connection still closes when there's an error
    except:
        # Disconnect from Arduino
        a.Q1(0)
        a.Q2(0)
        print('Error: Shutting down')
        a.close()
        raise