None
This notebook contains material from cbe30338-2021; content is available on Github.
# IMPORT DATA FILES USED BY THIS NOTEBOOK
import os, requests
file_links = [("data/Model_Data.csv", "https://jckantor.github.io/cbe30338-2021/data/Model_Data.csv")]
# This cell has been added by nbpages. Run this cell to download data files required for this notebook.
for filepath, fileurl in file_links:
stem, filename = os.path.split(filepath)
if stem:
if not os.path.exists(stem):
os.mkdir(stem)
if not os.path.isfile(filepath):
with open(filepath, 'wb') as f:
response = requests.get(fileurl)
f.write(response.content)
Models are essential for feedback control. It may be difficult to understand where and how the model was incorporated into the control system design, and the model may be just a simplistic cause-and-effect understanding of process operation, but be assured that somewhere in the control system is an embedded understanding of process operation.
The notebook is deep dive in creating models for the dynamic response of the Temperature Control Lab to power inputs and external disturbances. The survey of models include:
Data acquired through normal process operation or experimentation will normally be accessed from a process database. Here we demonstrate reading data stored previously stored in a .csv
file by a process historian.
%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
from scipy.optimize import least_squares
# read data file from the Github repository into a Pandas dataframe
data_location = "https://jckantor.github.io/cbe30338-2021/data/Model_Data.csv"
expt = pd.read_csv(data_location)
# set time index
expt = expt.set_index("Time")
t_expt = expt.index
# display data in tabular format
display(expt)
# simple plotting
expt.plot(grid=True, xlabel="time / seconds")
T1 | T2 | Q1 | Q2 | |
---|---|---|---|---|
Time | ||||
0.0 | 21.221 | 21.446 | 50.0 | 0.0 |
10.0 | 21.253 | 21.511 | 50.0 | 0.0 |
20.0 | 22.188 | 21.575 | 50.0 | 0.0 |
30.0 | 23.477 | 21.672 | 50.0 | 0.0 |
40.0 | 24.991 | 21.897 | 50.0 | 0.0 |
... | ... | ... | ... | ... |
560.0 | 52.803 | 36.722 | 50.0 | 0.0 |
570.0 | 52.803 | 36.786 | 50.0 | 0.0 |
580.0 | 52.803 | 36.754 | 50.0 | 0.0 |
590.0 | 53.028 | 37.012 | 50.0 | 0.0 |
600.0 | 52.642 | 36.947 | 50.0 | 0.0 |
61 rows × 4 columns
<AxesSubplot:xlabel='time / seconds'>
Study Question: "Break free from Excel". This exercise demonstrates a technique that you may find useful in your other coursework, particularly the Junior and Senior lab courses. The code cell below contains a link that will export data stored in a Google Sheet as a .csv
file. The file was previously created and share access set for viewing by anyone with a link. Use the Pandas library to read the data into a data frame, display the data table, set the data frame index to the column labeled Time
, and plot the data.
google_sheet_id = "1rV_oimh-GCYNP-xRP1HNrLvh7P4jZBSmwuqG3VPIaB0"
google_csv_download = f"https://docs.google.com/spreadsheets/d/{google_sheet_id}/gviz/tq?tqx=out:csv"
# write your code here
# create a function to compare two data sets
def plot_data(expt, pred):
t_expt = expt.index
# create a 3 x 1 grid of plot axes
fig, ax = plt.subplots(3, 1, figsize=(10,8))
# first plot axes
ax[0].scatter(t_expt, expt["T1"], label="T1 expt")
ax[0].scatter(t_expt, expt["T2"], label="T2 expt")
ax[0].plot(t_expt, pred["T1"], label="T1 pred")
ax[0].plot(t_expt, pred["T2"], label="T2 pred")
ax[0].set_ylabel("deg C")
ax[0].set_title("temperature response")
# second plot axes
ax[1].scatter(t_expt, expt["Q1"], label="Q1 expt")
ax[1].scatter(t_expt, expt["Q2"], label="Q2 expt")
ax[1].plot(t_expt, pred["Q1"], label="Q1 pred")
ax[1].plot(t_expt, pred["Q2"], label="Q2 pred")
ax[1].set_ylim([0, 100])
ax[1].set_ylabel("percent")
ax[1].set_title("heater power")
# third plot axes
ax[2].plot(t_expt, pred["T1"]-expt["T1"], label="T1 error")
ax[2].plot(t_expt, pred["T2"]-expt["T2"], label="T1 error")
ax[2].set_title("residuals")
# things to do for every plot axes
for a in ax:
a.grid(True)
a.set_xlabel("time / seconds")
a.legend()
plt.tight_layout()
return
# demonstrate by comparing experimental data to itself
plot_data(expt, expt);
We previously derived a model for the response T1 of the Temperature Control Lab to a constant input $\bar{u}_1$ as
$$\frac{dT_1}{dt} = -\frac{1}{\tau}(T_1 - T_{amb})+ \frac{K}{\tau}\bar{u}_1$$where the gain $K$ and the time constant $\tau$ are given by the
\begin{align} \tau = \frac{C_p}{U_a} \qquad K = \frac{\alpha P_1}{U_a} \end{align}Assuming the system starts with an initial condition $T_1(0) = T_{amb}$, the solution to the differential equation of the system is
\begin{align} T_1(t) & = T_{amb} + K\bar{u}_{1}(1 - e^{-(t-t_0)/\tau}) \end{align}Given estimates of $K$ and $\tau$, we can construct the preedicted response and compare to the experimental observed response.
# known parameters
T_amb = 21
u1bar = expt["Q1"].mean()
t_expt = expt.index
# adjustable model parameters
K = 0.5
tau = 100
# create function to create predictions and return residuals
def model_first_order(param, plot=False):
# unpack list of parameters into variables
K, tau = param
# create empty prediction data frame
pred = pd.DataFrame(columns=["T1", "T2", "Q1", "Q2"], index=t_expt)
# calculation prediction
pred["T1"] = T_amb + K*u1bar*(1 - np.exp(-t_expt/tau))
# plot if desired
if plot:
plot_data(expt, pred)
# return residual difference between prediction and experiment
return pred["T1"] - expt["T1"]
# compare prediction to experiment
model_first_order([K, tau], True)
Time 0.0 -0.221000 10.0 2.126065 20.0 3.343731 30.0 4.002544 40.0 4.250999 ... 560.0 -6.895447 570.0 -6.886649 580.0 -6.878689 590.0 -7.096486 600.0 -6.703969 Name: T1, Length: 61, dtype: float64
Study Question: By trial and error, attempt to find values for the gain $K$ and time constant $\tau$ that provide a useful prediction of the experimentally measured response.
Obviously, it would useful to have some way of finding 'optimal' values for $K$ and $\tau$. In this cell we use the least_squares
routine from scipy.optimize
to find optimal values of the parameters.
results = least_squares(model_first_order, [K, tau])
K, tau = results.x
print(f"K = {K}, tau = {tau}")
model_first_order(results.x, True)
K = 0.6810786742351254, tau = 196.3595429520904
Time 0.0 -0.221000 10.0 1.437844 20.0 2.109734 30.0 2.347839 40.0 2.285121 ... 560.0 0.284868 570.0 0.382487 580.0 0.475259 590.0 0.338425 600.0 0.808213 Name: T1, Length: 61, dtype: float64
A first-order with time delay (or 'dead time') are frequenty used in process control to model the delitreous
\begin{align} T_1(t) & = T_{amb} + K\bar{u}_{1}(1 - e^{-(t-t_{delay})/\tau}) \end{align}$$\frac{dT_1}{dt} = -\frac{1}{\tau}T_1 + \frac{K}{\tau}u_1(t-t_{delay}) + \frac{1}{\tau}T_{amb}$$# parameter values and units
T_amb = 21 # deg C
u1bar = expt["Q1"].mean()
t_expt = expt.index
# adjustable parameters
K = 0.6
tau = 150
t_delay = 10
def model_first_order_delay(param, plot=False):
K, tau, t_delay = param
pred = pd.DataFrame(columns=["T1", "T2", "Q1", "Q2"], index=t_expt)
pred["T1"] = [T_amb + K*u1bar*(1 - np.exp(-(t-t_delay)/tau)) if t > t_delay else T_amb for t in t_expt]
if plot:
plot_data(expt, pred)
return pred["T1"] - expt["T1"]
model_first_order_delay([K, tau, t_delay], True)
Time 0.0 -0.221000 10.0 -0.253000 20.0 0.746790 30.0 1.267800 40.0 1.447077 ... 560.0 -2.569846 570.0 -2.520390 580.0 -2.474123 590.0 -2.655840 600.0 -2.229349 Name: T1, Length: 61, dtype: float64
results = least_squares(model_first_order_delay, [K, tau, t_delay])
K, tau, t_delay = results.x
print(f"K = {K}, tau = {tau}, time delay = {t_delay}")
model_first_order_delay(results.x, True)
K = 0.6520728533493453, tau = 154.47860579512735, time delay = 21.772054298276494
Time 0.0 -0.221000 10.0 -0.253000 20.0 -1.188000 30.0 -0.785879 40.0 -0.362185 ... 560.0 -0.199623 570.0 -0.136923 580.0 -0.078154 590.0 -0.248068 600.0 0.189565 Name: T1, Length: 61, dtype: float64
We previously developed a model comprising a single differential equation for the temperature response of a heater/sensor assembly. Given $u_1(t)$ and an initial condition $T_{amb}$, the heater/sensor temperature is given by
$$\frac{dT_1}{dt} = -\frac{U_a}{C_p}T_1 + \frac{\alpha P_1}{C_p} u_1(t) + \frac{U_a}{C_p}T_{amb}$$Several of the key parameters are known. These include $\alpha$ which was determined by direct measurement, $P1$ which is set in the Arduino firmware. We assume the ambient temperature $T_{amb}$ is measurable and known.
The unknown parameters are the heat capacity $C_p$ and heat transfer coefficient $U_a$. We wish to find values of the unknown parameters that allow the model response to mimic responses measured by experiment.
Here we show a simulation of this model.
# parameter values and units
T_amb = 21 # deg C
P1 = 200 # P1 units
P2 = 100 # P2 units
alpha = 0.00016 # watts / (units P1 * percent U1)
# input function
def u1(t):
return np.interp(t, t_expt, expt["Q1"])
def u2(t):
return np.interp(t, t_expt, expt["Q2"])
The unknown parameters are the heat capacity $C_p$ and heat transfer coefficient $U_a$. We wish to find values of the unknown parameters that allow the model response to mimic responses measured by experiment.
Here we show a simulation of this model.
%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
from scipy.integrate import solve_ivp
# adjustable parameters
Cp = 6 # joules/deg C
Ua = 0.05 # watts/deg C
def model_energy_first_order(param, plot=False):
# unpack the adjustable parameters
Cp, Ua = param
# model solution
def deriv(t, y):
T1 = y[0]
return -(Ua/Cp)*T1 + (alpha*P1/Cp)*u1(t) + (Ua/Cp)*T_amb
soln = solve_ivp(deriv, [min(t_expt), max(t_expt)], [T_amb], t_eval=t_expt)
# create dataframe with predictions
pred = pd.DataFrame(columns=["T1", "T2", "Q1", "Q2"], index=t_expt)
pred["T1"] = soln.y[0]
# plot solution
if plot:
plot_data(expt, pred)
return pred["T1"] - expt["T1"]
model_energy_first_order([Cp, Ua], True)
Time 0.0 -0.221000 10.0 2.305582 20.0 3.724586 30.0 4.601597 40.0 5.082666 ... 560.0 -0.111357 570.0 -0.086947 580.0 -0.064244 590.0 -0.268356 600.0 0.136861 Name: T1, Length: 61, dtype: float64
results = least_squares(model_energy_first_order, [Cp, Ua])
Cp, Ua = results.x
print(f"Cp = {Cp}, Ua = {Ua}")
model_energy_first_order(results.x, True)
Cp = 9.229886459160445, Ua = 0.046971454569863326
Time 0.0 -0.221000 10.0 1.437129 20.0 2.108398 30.0 2.345976 40.0 2.283637 ... 560.0 0.285965 570.0 0.382852 580.0 0.475093 590.0 0.338030 600.0 0.808000 Name: T1, Length: 61, dtype: float64
The previous results are not yet fully satisfactory. We're still missing the initial 'lag' in response of the measured temperature.
For this third model, we consider the possibility that the heater and sensor may not be at the same temperature. In other words, that the heater/sensor assembly is not at a uniform temperature. To account for this possibility, we introduce $T_{H,1}$ to denote the temperature of heater one and $T_{S,1}$ to denote the temperature of the corresponding sensor. We'll further assume that sensor mainly exchanges heat with the heater, and the dominant heat transfer to the surroundings is through the heat sink attached to the heater.
This motivates a model
\begin{align} C^H_p\frac{dT_{H,1}}{dt} & = U_a(T_{amb} - T_{H,1}) + U_b(T_{S,1} - T_{H,1}) + \alpha P_1u_1\\ C^S_p\frac{dT_{S,1}}{dt} & = U_b(T_{H,1} - T_{S,1}) \end{align}where $C^H_p$ and $C^S_p$ are the gross heat capacities of the heater and sensor, respectively, and $U_b$ is a new heat transfer coefficient characterizing the exchange of heat between the heater and sensor.
\begin{align} \frac{dT_{H,1}}{dt} & = -\frac{U_a+U_b}{C^H_p}T_{H,1} + \frac{U_b}{C^H_p}T_{S,1} + \frac{\alpha P_1}{C^H_p}u_1 + \frac{U_a}{C^H_p}T_{amb}\\ \frac{dT_{S,1}}{dt} & = \frac{U_b}{C^S_p}(T_{H,1} - T_{S,1}) \end{align}Where measured temperature, that is, the temperature recorded by the Arduino, $T_1$ is given by
$$T_1 = T_{S,1}$$%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
from scipy.integrate import solve_ivp
# adjustable parameters
CpH = 5 # joules/deg C
CpS = 1 # joules/deg C
Ua = 0.05 # watts/deg C
Ub = 0.05 # watts/deg C
def model_energy_second_order(param, plot=False):
# unpack the adjustable parameters
CpH, CpS, Ua, Ub = param
# model solution
def deriv(t, y):
T1H, T1S = y
dT1H = (-(Ua + Ub)*T1H + Ub*T1S + alpha*P1*u1(t) + Ua*T_amb)/CpH
dT1S = Ub*(T1H - T1S)/CpS
return [dT1H, dT1S]
soln = solve_ivp(deriv, [min(t_expt), max(t_expt)], [T_amb, T_amb], t_eval=t_expt)
# create dataframe with predictions
pred = pd.DataFrame(columns=["T1", "T2", "Q1", "Q2"], index=t_expt)
pred["T1"] = soln.y[1]
# plot solution
if plot:
ax = plot_data(expt, pred)
return pred["T1"] - expt["T1"]
model_energy_second_order([CpH, CpS, Ua, Ub], plot=True)
Time 0.0 -0.221000 10.0 0.371773 20.0 0.895562 30.0 1.395004 40.0 1.774688 ... 560.0 -0.210368 570.0 -0.188967 580.0 -0.148499 590.0 -0.323733 600.0 0.093363 Name: T1, Length: 61, dtype: float64
results = least_squares(model_energy_second_order, [CpH, CpS, Ua, Ub])
CpH, CpS, Ua, Ub = results.x
print(f"CpH = {CpH}, CpS = {CpS}, Ua = {Ua}, Ub = {Ub}")
model_energy_second_order(results.x, True)
CpH = 2.1819127156760283, CpS = 1.878312055293452, Ua = 0.04956661570349656, Ub = 0.021187799850674392
Time 0.0 -0.221000 10.0 0.104481 20.0 0.063797 30.0 -0.007664 40.0 -0.117127 ... 560.0 -0.289049 570.0 -0.245878 580.0 -0.203222 590.0 -0.386367 600.0 0.039925 Name: T1, Length: 61, dtype: float64
where
\begin{align} T_1 & = T_{S,1} \\ T_2 & = T_{S,2} \end{align}where
\begin{align} T_1 & = T_{S,1} \\ T_2 & = T_{S,2} \end{align}%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
from scipy.integrate import solve_ivp
# adjustable parameters
CpH = 5 # joules/deg C
CpS = 1 # joules/deg C
Ua = 0.05 # watts/deg C
Ub = 0.05 # watts/deg C
Uc = 0.05 # watts/deg C
def model_energy_fourth_order(param, plot=False):
# unpack the adjustable parameters
CpH, CpS, Ua, Ub, Uc = param
# model solution
def deriv(t, y):
T1H, T1S, T2H, T2S= y
dT1H = (-(Ua + Ub + Uc)*T1H + Ub*T1S + Uc*T2H + alpha*P1*u1(t) + Ua*T_amb)/CpH
dT1S = Ub*(T1H - T1S)/CpS
dT2H = (-(Ua + Ub + Uc)*T2H + Ub*T2S + Uc*T1H + alpha*P2*u2(t) + Ua*T_amb)/CpH
dT2S = Ub*(T2H - T2S)/CpS
return [dT1H, dT1S, dT2H, dT2S]
soln = solve_ivp(deriv, [min(t_expt), max(t_expt)], [T_amb]*4, t_eval=t_expt)
# create dataframe with predictions
pred = pd.DataFrame(columns=["T1", "T2", "Q1", "Q2"], index=t_expt)
pred["T1"] = soln.y[1]
pred["T2"] = soln.y[3]
# plot solution
if plot:
ax = plot_data(expt, pred)
err1 = np.array(pred["T1"] - expt["T1"])
err2 = np.array(pred["T2"] - expt["T2"])
return np.concatenate((err1, err2))
model_energy_fourth_order([CpH, CpS, Ua, Ub, Uc], plot=True)
array([ -0.221 , 0.34640496, 0.76940428, 1.05824442, 1.13171249, 1.24645305, 1.07316775, 0.81586616, 0.48007917, -0.13344645, -0.48890253, -0.70921588, -1.25124638, -1.76745403, -2.20823788, -2.69742627, -3.14122854, -3.52330422, -4.01831601, -4.40206324, -4.88886002, -5.19721912, -5.48385191, -5.96815841, -6.40553965, -6.57439776, -6.70435903, -6.9013846 , -7.2455444 , -7.69003022, -7.92579427, -8.09662387, -8.30244458, -8.28467125, -8.45397113, -8.44203263, -8.55229841, -8.76329137, -8.79264212, -8.97090733, -9.05270848, -9.21073629, -9.11325468, -9.42975647, -9.44638557, -9.61415333, -9.564619 , -9.87641255, -10.01102868, -9.74099527, -9.82511837, -9.804095 , -10.08438906, -10.11845575, -10.39033103, -10.67585924, -10.64774194, -10.64625393, -10.63882106, -10.84765596, -10.44699971, -0.446 , -0.48503157, -0.44947717, -0.33520192, -0.25415283, -0.1348096 , 0.0347824 , 0.13562221, 0.28798863, 0.38045703, 0.27346487, 0.35047914, 0.34883465, 0.29794038, 0.25683209, 0.12771424, 0.13995666, -0.08773217, -0.13871927, -0.18724166, -0.46082221, -0.63253118, -0.84498618, -0.98516443, -1.17491935, -1.33677645, -1.49587723, -1.8492109 , -1.96193909, -1.99769309, -2.25902565, -2.50785808, -2.61676844, -2.80891642, -2.99064871, -3.17264735, -3.21202536, -3.45674232, -3.57654781, -3.68625433, -3.83610915, -3.81942424, -4.06981347, -4.11040659, -4.07154664, -4.2405798 , -4.40609913, -4.60440012, -4.66476863, -4.65018425, -4.83416322, -4.9332353 , -4.99274741, -4.94824326, -5.08791481, -5.21768802, -5.27702057, -5.31165008, -5.25854906, -5.50630469, -5.43159342])
results = least_squares(model_energy_fourth_order, [CpH, CpS, Ua, Ub, Uc])
CpH, CpS, Ua, Ub, Uc = results.x
print(f"CpH = {CpH}, CpS = {CpS}, Ua = {Ua}, Ub = {Ub}, Uc = {Uc}")
model_energy_fourth_order(results.x, True)
CpH = 4.4642229433310705, CpS = 0.8190620772014501, Ua = 0.032226735496058236, Ub = 0.01862452210295832, Uc = 0.033554024370968105
array([-0.221 , 0.10032875, 0.05752008, -0.00405231, -0.10773464, 0.01920575, 0.02181627, 0.03268211, 0.05934022, -0.12621606, -0.02560712, 0.22155593, 0.1591108 , 0.12013875, 0.14267453, 0.09875163, 0.08540225, 0.13037096, 0.05135871, 0.04902892, -0.09703112, -0.09303583, -0.06500128, -0.20674525, -0.32566391, -0.22615469, -0.12616143, -0.08732116, -0.16917961, -0.37119119, -0.39871904, -0.38303497, -0.3863195 , -0.15266185, -0.13605994, 0.01670928, 0.02122981, -0.05314745, 0.08779108, 0.06239935, 0.09510954, 0.02842961, 0.244774 , 0.09824684, 0.2274448 , 0.14545713, 0.23302579, -0.01549275, -0.04315274, 0.31607625, 0.29086137, 0.36550651, 0.18295226, 0.25477598, 0.05819174, -0.19055652, -0.12624109, -0.06348875, -0.00238632, -0.16838589, 0.27369513, -0.446 , -0.50119113, -0.51353834, -0.49453376, -0.52704126, -0.5249296 , -0.46191438, -0.43668769, -0.33177506, -0.25803356, -0.35343691, -0.23397058, -0.16735761, -0.12678506, -0.05492989, -0.05271973, 0.10466706, 0.0275704 , 0.13124193, 0.25303202, 0.16776558, 0.19461196, 0.17308493, 0.19904272, 0.17668492, 0.20215753, 0.24412295, 0.07539325, 0.11601581, 0.23727338, 0.14868402, 0.07800115, 0.12121353, 0.05554524, 0.01045571, -0.01114647, 0.12013079, 0.01510873, -0.00699348, -0.01320108, -0.03383083, 0.12440592, -0.01963049, -0.01180309, 0.08812169, 0.02647401, 0.00262072, -0.08701869, -0.08883899, -0.00579806, -0.09870102, -0.10820023, -0.12779525, -0.05783289, -0.14850718, -0.19822316, -0.18305994, -0.17332694, -0.07350994, -0.26875958, -0.14489134])
Create one or more notebooks to show your results for the following exercises. The model you develop in this exercise will be used in the remainder of the semester for simulation and control design for your copy of the Temperature Control Laboratory.
Write two python functions, one called u1(t) and the other u2(t), that create the following input signals that will be used to test the Temperature Control Lab.
Develop these functions, and plot the results for t=-100 to t=1800 (yes, beyond the end of the experiment) showing correct performance.
Use the functions created in Exercise 1 to obtain experimental data for your Temperature Control Lab. This will take some time (about 1/2 hour if everything works perfectly). Record T1, T2, Q1, Q2 using the Historian. Be careful to ...
Save this data to a .csv file.
To avoid overwriting or damaging the data you collected in Exercise 2, create a separate notebook for model fitting. Fit two models to your data:
Fit the first-order plus delay model to the results for "T1" using data up to t=620 (i.e, the data before you turn on the second heater.). What are the gain, time-constant, and time delay for your device?
Fit the fourth-order model (alternatively, the state-space model if you're comfortable with that formulation), and report all heat capacities, heat transfer coefficients. Note that you're fitting a two-input, two-output model which is very challenging. So the fit may not look quite as good as those shown above when u2(t) was held constant.