4.2. Two State Model for a Single Heater#

4.2.1. Dynamics of the Heater/Sensor System#

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{split} \begin{align} C^H_p\frac{dT_{H,1}}{dt} & = U_a(T_{amb} - T_{H,1}) + U_c(T_{S,1} - T_{H,1}) + P_1u_1\\ C^S_p\frac{dT_{S,1}}{dt} & = U_c(T_{H,1} - T_{S,1}) \end{align} \end{split}\]

where \(C^H_p\) and \(C^S_p\) are the gross heat capacities of the heater and sensor, respectively, and \(U_c\) is a new heat transfer coefficient characterizing the exchange of heat between the heater and sensor.

4.2.2. Model in deviation variables#

Following the approach demonstrated in the first two models, we construct a model in terms of deviation from ambient temperature.

\[\begin{split} \begin{align} \frac{dT_{H,1}'}{dt} & = -\frac{U_a+U_c}{C^H_p}T_{H,1}' + \frac{U_c}{C^H_p}T_{S,1}' + P_1u_1\\ \frac{dT_{S,1}'}{dt} & = \frac{U_c}{C^S_p}(T_{H,1}' - T_{S,1}') \end{align} \end{split}\]

Next we jump right to the step of tuning the model parameters to fit the step response data.

%matplotlib inline
import pandas as pd
import matplotlib.pyplot as plt
from scipy.integrate import odeint
from ipywidgets import interact

data = pd.read_csv('Step_Test_Data.csv').set_index('Time')[1:]
t = data.index
T1 = data['T1'].values
T2 = data['T2'].values

# known parameter values
P1 = 4
u1 = 0.5   # steady state value of u1 (fraction of total power)
P2 = 2
u2 = 0.0
T_ambient = 21

def compare(Ua, Uc, Cp_H, Cp_S):
    def deriv(T,t):
        T_H1, T_S1 = T
        dT_H1 = -(Ua + Uc)*T_H1/Cp_H + Uc*T_S1/Cp_H + P1*u1/Cp_H
        dT_S1 = Uc*T_H1/Cp_S - Uc*T_S1/Cp_S
        return [dT_H1, dT_S1]
    T = odeint(deriv, [0,0], t)
    # plot
    plt.figure(figsize=(10,6))
    plt.subplot(2,1,1)
    plt.plot(t, T[:,1] + T_ambient, t, T1)
    plt.xlabel('Time / seconds')
    plt.ylabel('Temperature / °C')
    plt.title('Fitting model to Step Test Data for Heater 1')
    plt.grid()
    plt.text(300, 40, 'Ua = ' + str(Ua))
    plt.text(300, 35, 'Uc = ' + str(Uc))
    plt.text(300, 30, 'Cp_H = ' + str(Cp_H))
    plt.text(300, 25, 'Cp_S = ' + str(Cp_S))
    plt.subplot(2,1,2)
    plt.plot(t, T[:,1] + T_ambient - T1)
    plt.title('Model Error')
    plt.xlabel('Time / seconds')
    plt.ylabel('Temperature / °C')
    plt.grid()
    plt.tight_layout()

interact(compare, 
         Ua=(0,0.08,0.001), 
         Uc=(0,0.06,0.001), 
         Cp_H=(3,11,0.1), 
         Cp_S = (0.1,2,.01))
---------------------------------------------------------------------------
FileNotFoundError                         Traceback (most recent call last)
/var/folders/cm/z3t28j296f98jdp1vqyplkz00000gn/T/ipykernel_39317/3761358207.py in <module>
      5 from ipywidgets import interact
      6 
----> 7 data = pd.read_csv('Step_Test_Data.csv').set_index('Time')[1:]
      8 t = data.index
      9 T1 = data['T1'].values

~/opt/anaconda3/lib/python3.9/site-packages/pandas/util/_decorators.py in wrapper(*args, **kwargs)
    309                     stacklevel=stacklevel,
    310                 )
--> 311             return func(*args, **kwargs)
    312 
    313         return wrapper

~/opt/anaconda3/lib/python3.9/site-packages/pandas/io/parsers/readers.py in read_csv(filepath_or_buffer, sep, delimiter, header, names, index_col, usecols, squeeze, prefix, mangle_dupe_cols, dtype, engine, converters, true_values, false_values, skipinitialspace, skiprows, skipfooter, nrows, na_values, keep_default_na, na_filter, verbose, skip_blank_lines, parse_dates, infer_datetime_format, keep_date_col, date_parser, dayfirst, cache_dates, iterator, chunksize, compression, thousands, decimal, lineterminator, quotechar, quoting, doublequote, escapechar, comment, encoding, encoding_errors, dialect, error_bad_lines, warn_bad_lines, on_bad_lines, delim_whitespace, low_memory, memory_map, float_precision, storage_options)
    584     kwds.update(kwds_defaults)
    585 
--> 586     return _read(filepath_or_buffer, kwds)
    587 
    588 

~/opt/anaconda3/lib/python3.9/site-packages/pandas/io/parsers/readers.py in _read(filepath_or_buffer, kwds)
    480 
    481     # Create the parser.
--> 482     parser = TextFileReader(filepath_or_buffer, **kwds)
    483 
    484     if chunksize or iterator:

~/opt/anaconda3/lib/python3.9/site-packages/pandas/io/parsers/readers.py in __init__(self, f, engine, **kwds)
    809             self.options["has_index_names"] = kwds["has_index_names"]
    810 
--> 811         self._engine = self._make_engine(self.engine)
    812 
    813     def close(self):

~/opt/anaconda3/lib/python3.9/site-packages/pandas/io/parsers/readers.py in _make_engine(self, engine)
   1038             )
   1039         # error: Too many arguments for "ParserBase"
-> 1040         return mapping[engine](self.f, **self.options)  # type: ignore[call-arg]
   1041 
   1042     def _failover_to_python(self):

~/opt/anaconda3/lib/python3.9/site-packages/pandas/io/parsers/c_parser_wrapper.py in __init__(self, src, **kwds)
     49 
     50         # open handles
---> 51         self._open_handles(src, kwds)
     52         assert self.handles is not None
     53 

~/opt/anaconda3/lib/python3.9/site-packages/pandas/io/parsers/base_parser.py in _open_handles(self, src, kwds)
    220         Let the readers open IOHandles after they are done with their potential raises.
    221         """
--> 222         self.handles = get_handle(
    223             src,
    224             "r",

~/opt/anaconda3/lib/python3.9/site-packages/pandas/io/common.py in get_handle(path_or_buf, mode, encoding, compression, memory_map, is_text, errors, storage_options)
    700         if ioargs.encoding and "b" not in ioargs.mode:
    701             # Encoding
--> 702             handle = open(
    703                 handle,
    704                 ioargs.mode,

FileNotFoundError: [Errno 2] No such file or directory: 'Step_Test_Data.csv'

4.2.3. Consequences of this Model#

An important consequence of this model is that temperature reported by sensor is not keep up with the heater temperature during rapid transients. To see this, below we plot the measured temperature and the predicted heater temperature.

# fitted parameters
Ua = 0.058
Uc = 0.051
Cp_H = 7.0
Cp_S = 1.12

# known parameter values
P1 = 4
u1 = 0.5   # steady state value of u1 (fraction of total power)
P2 = 2
u2 = 0.0
T_ambient = 21

def deriv(T,t):
    T_H1, T_S1 = T
    dT_H1 = -(Ua + Uc)*T_H1/Cp_H + Uc*T_S1/Cp_H + P1*u1/Cp_H
    dT_S1 = Uc*T_H1/Cp_S - Uc*T_S1/Cp_S
    return [dT_H1, dT_S1]
T = odeint(deriv, [0,0], t)

plt.figure(figsize=(10,6))
plt.subplot(2,1,1)
plt.plot(t, T1, t, T[:,0]+T_ambient)
plt.xlabel('Time / seconds')
plt.ylabel('Temperature / °C')
plt.legend(['Measured Temperature','Predicted Heater Temperature'])
plt.grid()

plt.subplot(2,1,2)
plt.plot(t, T[:,0] + T_ambient - T1)
plt.title('Sensor Error')
plt.xlabel('Time / seconds')
plt.ylabel('Temperature / °C')
plt.grid()
plt.tight_layout()
---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
/var/folders/cm/z3t28j296f98jdp1vqyplkz00000gn/T/ipykernel_39317/1830847675.py in <module>
     17     dT_S1 = Uc*T_H1/Cp_S - Uc*T_S1/Cp_S
     18     return [dT_H1, dT_S1]
---> 19 T = odeint(deriv, [0,0], t)
     20 
     21 plt.figure(figsize=(10,6))

NameError: name 't' is not defined

In some applications, such as crystallization, fermentation and separations, a mismatch of 4°C can be very significant.