Agenda¶
Part A -- The manometer: an inherently second-order system (Sec. 7.1)
Part B -- Reading an underdamped response: overshoot, decay ratio, period (Sec. 7.1)
Part C -- Transportation lag and its Pade approximations (Sec. 7.2)
Part D -- The control system: the stirred-tank heater as a block diagram (Ch. 8)
Setup (run once)¶
- The standard imports, as in Lecture 3
- In Colab, run
%pip install controlfirst
import numpy as np
import matplotlib.pyplot as plt
import sympy as sp
import control as ct
from scipy.integrate import solve_ivp
from scipy.optimize import curve_fit
plt.rcParams.update({"font.size": 15})
Introduce -- why does a manometer reading oscillate? (Fig. 7-1)¶
- A U-tube, liquid column of length $L$, tube diameter $D$.
- At $t = 0$ a pressure difference $\Delta P$ is applied.
- The liquid has inertia and friction: it can overshoot.
- Assumptions: laminar flow; Hagen-Poiseuille friction at every instant; momentum correction $\beta = 4/3$.
Textbook figure: Coughanowr Fig. 7-1 (manometer).
Derive 1/3 -- the force balance (Eq. 7.3)¶
Rate of change of momentum = pressure force $-$ weight $-$ wall friction; interface velocity $\bar V = \frac12 \frac{dh}{dt}$:
$$\rho\frac{\pi D^2}{4}L\cdot\frac43\cdot\frac12\ddot h = \Delta P\frac{\pi D^2}{4} - \rho gh\frac{\pi D^2}{4} - \frac{8\mu}{D}\cdot\frac12\dot h\cdot\pi DL$$
Derive 2/3 -- standard form¶
Divide by $\rho g\,\pi D^2/4$:
$$\underbrace{\frac{2L}{3g}}_{\tau^2}\ddot h + \underbrace{\frac{16\mu L}{\rho D^2 g}}_{2\zeta\tau}\dot h + h = \frac{\Delta P}{\rho g} \;\Rightarrow\; \tau = \sqrt{\frac{2L}{3g}},\quad \zeta = \frac{8\mu}{\rho D^2}\sqrt{\frac{3L}{2g}}$$
Derive 3/3 -- transfer function and its roots¶
Zero initial conditions, Laplace transform:
$$\frac{Y(s)}{X(s)} = \frac{1}{\tau^2s^2 + 2\zeta\tau s + 1} \qquad (7.12)$$
Roots $s = \left(-\zeta \pm \sqrt{\zeta^2 - 1}\right)/\tau$: complex if $\zeta < 1$ (underdamped), equal if $\zeta = 1$ (critical), real if $\zeta > 1$ (overdamped). Table 7.1.
Code 1/3 -- from force balance to $\tau$ and $\zeta$¶
- Mirrors Derive 1/3-2/3: divide the balance, match coefficients
sp.solveon two equations returns $\tau$ and $\zeta$ together
L, g, mu, rho, D = sp.symbols("L g mu rho D", positive=True)
tau, zeta = sp.symbols("tau zeta", positive=True)
a2 = sp.Rational(2, 3) * L / g # coefficient of h''
a1 = 16 * mu * L / (rho * D**2 * g) # coefficient of h'
sol = sp.solve([sp.Eq(tau**2, a2), sp.Eq(2*zeta*tau, a1)],
[tau, zeta], dict=True)[0]
sol
{tau: sqrt(6)*sqrt(L)/(3*sqrt(g)),
zeta: 4*sqrt(6)*sqrt(L)*mu/(D**2*sqrt(g)*rho)}
Code 2/3 -- the book's three manometers¶
- Water, $L = 200$ cm, three tube diameters (cgs units)
- Substitute the numbers into the formulas from Code 1/3
data = {L: 200, g: 980, mu: 0.01, rho: 1.0} # cm, g, s
for Dv in [0.11, 0.21, 0.31]: # cm
tv = float(sol[tau].subs(data))
zv = float(sol[zeta].subs(data | {D: Dv}))
print(f"D = {Dv} cm: tau = {tv:.3f} s, zeta = {zv:.2f}")
D = 0.11 cm: tau = 0.369 s, zeta = 3.66 D = 0.21 cm: tau = 0.369 s, zeta = 1.00 D = 0.31 cm: tau = 0.369 s, zeta = 0.46
Code 3/3 -- the roots decide the case¶
- Mirrors Derive 3/3: roots of $\tau^2s^2 + 2\zeta\tau s + 1$
np.iscomplextells underdamped from overdamped
tv = 0.369
for zv in [3.66, 1.00, 0.46]:
r = np.roots([tv**2, 2 * zv * tv, 1])
kind = "complex" if np.iscomplex(r).any() else "real"
print(f"zeta = {zv}: roots {np.round(r, 2)} ({kind})")
zeta = 3.66: roots [-19.46 -0.38] (real) zeta = 1.0: roots [-2.71 -2.71] (real) zeta = 0.46: roots [-1.25+2.41j -1.25-2.41j] (complex)
Build on it -- the step responses (cf. Fig. 7-5)¶
- One
ct.tfper manometer, a 10 cm step; no inversion by hand
tt = np.linspace(0, 4, 400) # s
fig, ax = plt.subplots(figsize=(7, 5.25))
for Dv, zv in [(0.11, 3.66), (0.21, 1.00), (0.31, 0.46)]:
G = ct.tf([1], [tv**2, 2 * zv * tv, 1])
y = ct.step_response(10 * G, tt).outputs # 10 cm step
ax.plot(tt, y, label=f"D = {Dv} cm, zeta = {zv}")
ax.axhline(10, ls="--", c="gray")
ax.set(xlabel="t (s)", ylabel="h (cm)")
ax.legend(loc="lower right")
plt.show()
Your Turn (8 min)¶
- By hand: same water manometer, $D = 0.25$ cm. Find $\zeta$ ($\zeta = 0.0443/D^2$). Under-, critically or overdamped?
- Read the code: real or complex? (No need for the numbers.)
print(np.roots([0.136, 0.340, 1]))
Check -- cold-call¶
A snubber is a partly closed valve placed in the bend of the tube. Which parameter does it change -- $\tau$ or $\zeta$ -- and in which direction? Why does that make the gauge easier to read?
Introduce -- numbers that describe a wiggle (Fig. 7-6)¶
- Overshoot $A/B$; decay ratio $C/A$; period $T$; rise time $t_r$ (first reaching the final value); response time (within $\pm 5\%$ for good).
- Why care: a reactor temperature may not exceed a catalyst limit; a tank may not overflow.
- Unit step response for $\zeta < 1$ (Eq. 7.18):
$$Y = 1 - \frac{e^{-\zeta t/\tau}}{\sqrt{1-\zeta^2}} \sin\!\left(\sqrt{1-\zeta^2}\,\frac{t}{\tau} + \tan^{-1}\frac{\sqrt{1-\zeta^2}}{\zeta}\right)$$
Derive 1/3 -- where are the peaks?¶
Differentiate Eq. 7.18 and set $dY/dt = 0$:
$$\frac{dY}{dt} = \frac{e^{-\zeta t/\tau}}{\tau\sqrt{1-\zeta^2}} \sin\!\left(\sqrt{1-\zeta^2}\,\frac{t}{\tau}\right) = 0 \;\Rightarrow\; t_p = \frac{\pi\tau}{\sqrt{1-\zeta^2}}$$
Derive 2/3 -- overshoot and decay ratio¶
Evaluate $Y(t_p) - 1$; the next peak is one period later:
$$\text{overshoot} = \exp\!\left(\frac{-\pi\zeta}{\sqrt{1-\zeta^2}}\right) \;(7.25), \qquad \text{decay ratio} = (\text{overshoot})^2 \;(7.26)$$
Derive 3/3 -- period of oscillation¶
The coefficient of $t$ inside the sine is the radian frequency:
$$\omega = \frac{\sqrt{1-\zeta^2}}{\tau}\;(7.27), \qquad T = \frac{2\pi\tau}{\sqrt{1-\zeta^2}}\;(7.28)$$
At $\zeta = 0$: the natural period $T_n = 2\pi\tau$.
Code 1/3 -- differentiate Eq. 7.18¶
- Mirrors Derive 1/3: type Eq. 7.18, let SymPy differentiate
- Declaring $0 < \zeta$ lets the simplification go through
t = sp.symbols("t", positive=True)
wd = sp.sqrt(1 - zeta**2) # damped frequency * tau
Y = 1 - sp.exp(-zeta*t/tau) / wd * sp.sin(wd*t/tau + sp.atan(wd/zeta))
dY = sp.simplify(sp.diff(Y, t))
dY
Code 2/3 -- overshoot from the formula¶
- Mirrors Derive 2/3: substitute the first peak time into $Y - 1$
tp = sp.pi * tau / wd # first peak
overshoot = sp.simplify(Y.subs(t, tp) - 1)
print(overshoot) # Eq. 7.25
print(sp.simplify(overshoot**2)) # Eq. 7.26
exp(-pi*zeta/sqrt(1 - zeta**2)) exp(-2*pi*zeta/sqrt(1 - zeta**2))
Code 3/3 -- Prob. 7.1 by formula and by simulation¶
- $Y/X = 10/(s^2 + 1.6s + 4) = 2.5/(0.25s^2 + 0.4s + 1)$: $\tau = 0.5$, $\zeta = 0.4$
- Step of 4;
ct.step_infomeasures the simulated response
tv, zv, K, M = 0.5, 0.4, 2.5, 4.0
os_ = np.exp(-np.pi * zv / np.sqrt(1 - zv**2)) # Eq. 7.25
T = 2 * np.pi * tv / np.sqrt(1 - zv**2) # Eq. 7.28
print(f"formula: max {K*M*(1 + os_):.2f}, period {T:.2f}")
info = ct.step_info(M * ct.tf([10], [1, 1.6, 4]))
print(f"measured: max {info['Peak']:.2f}, "
f"peak at {info['PeakTime']:.2f}")
formula: max 12.54, period 3.43 measured: max 12.53, peak at 1.74
Build on it -- a damping-ratio sweep (cf. Fig. 7-3)¶
- Plot $Y$ against $t/\tau$ for six values of $\zeta$
tt = np.linspace(0, 15, 500) # t / tau
fig, ax = plt.subplots(figsize=(7, 5.25))
for zv in [0.2, 0.4, 0.6, 0.8, 1.0, 2.0]:
y = ct.step_response(ct.tf([1], [1, 2*zv, 1]), tt).outputs
ax.plot(tt, y, label=f"zeta = {zv}")
ax.set(xlabel="t / tau", ylabel="Y(t)")
ax.legend(loc="lower right", ncol=2, fontsize=11)
plt.show()
Your Turn (8 min)¶
Prob. 7.15. $Y/X = 10/(2s^2 + 0.3s + 0.5)$, step of 3.
- By hand: standard form, $\tau$, $\zeta$, overshoot and the radian frequency of oscillation.
- Read the code: what does this print, to two decimals?
print(np.exp(-np.pi * 0.15 / np.sqrt(1 - 0.15**2)))
Check -- poll¶
You may not overshoot at all, but want the fastest approach to the new value. Which $\zeta$ do you choose?
A) 0.4 B) 0.7 C) 1.0 D) 2.0
Introduce -- liquid in a pipe (Fig. 7-9)¶
- Plug flow through an insulated pipe, volume $AL$, flow $q$.
- Whatever enters, leaves -- unchanged but late: $y(t) = x(t - \tau)$, $\tau = AL/q$.
- Also called dead time. It makes control hard (Ch. 16-17).
Derive 1/2 -- the transfer function of a delay¶
Translation theorem (App. 3A): $\mathcal{L}\{X(t-\tau)\} = e^{-\tau s}X(s)$:
$$\frac{Y(s)}{X(s)} = e^{-\tau s} \qquad (7.46)$$
Not a ratio of polynomials: Routh (Ch. 13) cannot handle it.
Derive 2/2 -- rational approximations¶
Write $e^{-\tau s} = e^{-\tau s/2}/e^{\tau s/2}$ and expand both:
$$e^{-\tau s}\approx\frac{1}{1+\tau s}\;(7.47),\quad \frac{1-\tau s/2}{1+\tau s/2}\;(7.48),\quad \frac{1-\frac{\tau s}{2}+\frac{\tau^2s^2}{12}} {1+\frac{\tau s}{2}+\frac{\tau^2s^2}{12}}\;(7.49)$$
Code 1/2 -- how many Taylor terms does each match?¶
- Mirrors Derive 2/2: expand each approximation in powers of $s$
- Subtract the exact series; the first surviving power tells the order
s = sp.symbols("s")
exact = sp.exp(-tau * s)
approx = {"7.47": 1 / (1 + tau*s),
"7.48": (1 - tau*s/2) / (1 + tau*s/2),
"7.49": (1 - tau*s/2 + (tau*s)**2/12)
/ (1 + tau*s/2 + (tau*s)**2/12)}
for name, f in approx.items():
err = sp.series(f - exact, s, 0, 6).removeO()
(power,), coeff = sp.Poly(err, s).terms()[-1] # lowest power
print(name, "error starts at", coeff * s**power)
7.47 error starts at s**2*tau**2/2 7.48 error starts at -s**3*tau**3/12 7.49 error starts at s**5*tau**5/720
Code 2/2 -- ct.pade returns the same coefficients¶
ct.pade(T, n)gives numerator and denominator lists- Compare with Eqs. 7.48 and 7.49 for $\tau = 2$
for n in [1, 2]:
num, den = ct.pade(2.0, n) # tau = 2
print(f"order {n}: num {np.round(num, 3)}, "
f"den {np.round(den, 3)}")
order 1: num [-1. 1.], den [1. 1.] order 2: num [ 1. -3. 3.], den [1. 3. 3.]
Build on it -- step responses of the approximations (Fig. 7-11)¶
- The true delay is a shifted step; plot it beside the three
tt = np.linspace(0, 6, 600)
fig, ax = plt.subplots(figsize=(7, 5.25))
ax.plot(tt, (tt >= 2.0).astype(float), "k", lw=3, label="e^(-2s)")
cands = {"1/(2s+1)": ct.tf([1], [2, 1]),
"Pade 1": ct.tf(*ct.pade(2.0, 1)),
"Pade 2": ct.tf(*ct.pade(2.0, 2))}
for name, G in cands.items():
ax.plot(tt, ct.step_response(G, tt).outputs, label=name)
ax.set(xlabel="t", ylabel="response to a unit step")
ax.legend(loc="lower right", fontsize=11)
plt.show()
Build on it -- four tanks look like dead time plus one lag¶
- Callback to Lecture 3: four noninteracting tanks, $\tau = 1$
- Fit $K e^{-\theta s}/(\tau s + 1)$ (FOPDT) with
curve_fit
tt = np.linspace(0, 15, 301)
y4 = ct.step_response(ct.tf([1], [1, 1])**4, tt).outputs
def fopdt(t, K, tau_f, theta):
return np.where(t > theta,
K * (1 - np.exp(-(t - theta) / tau_f)), 0.0)
p, _ = curve_fit(fopdt, tt, y4, p0=[1, 2, 1])
print("K = {:.2f}, tau = {:.2f}, theta = {:.2f}".format(*p))
print(f"worst error {np.abs(fopdt(tt, *p) - y4).max():.3f}")
K = 1.03, tau = 2.60, theta = 1.74 worst error 0.098
Your Turn (8 min)¶
- By hand: water flows at $q = 0.5$ L/s through a pipe of volume 2 L. Find $\tau$ and write the first-order Pade approximation.
- Read the code: what are
numandden?
num, den = ct.pade(4.0, 1)
Check -- think-pair-share¶
The first-order Pade approximation's step response first goes the wrong way (to $-1$). What feature of $(1 - \tau s/2)/(1 + \tau s/2)$ causes that -- and why would a controller find it confusing?
Introduce -- four components, one loop (Fig. 8-1, 8-2)¶
- Process: stirred-tank heater, inlet $T_i$, heat input $q$.
- Measuring element: thermocouple, reading $T_m$.
- Controller: error $\varepsilon = T_R - T_m$, proportional law.
- Final control element: heater (or steam valve).
- Servo problem: follow set-point changes. Regulator problem: reject load changes ($T_i$).
Derive 1/3 -- the process block (Eq. 8.10)¶
Energy balance, subtract steady state, deviation variables, transform:
$$Q + wC(T_i' - T') = \rho CV\frac{dT'}{dt} \;\Rightarrow\; T'(s) = \frac{1/wC}{\tau s+1}Q(s) + \frac{1}{\tau s+1}T_i'(s)$$
$\tau = \rho V/w$. Two inputs; their effects add (superposition).
Derive 2/3 -- Example 8.1 numbers¶
1000 L, 200 L/min, water, $T_s = 80$ C, $T_{is} = 60$ C:
$\tau = 5$ min, $\;1/wC = 1/(14\ \text{kW/C})$, $\;q_s = wC(80 - 60) = 280$ kW.
$T_i$ +10 C: $T' = 10(1 - e^{-t/5})$. $\;Q$ +42 kW: $T' = 3(1 - e^{-t/5})$. Both: $13(1 - e^{-t/5})$.
Derive 3/3 -- sensor and controller blocks¶
Ex. 8.2: thermocouple 90% complete in 45 s, first order:
$$0.9 = 1 - e^{-45/\tau_m} \Rightarrow \tau_m = \frac{45}{\ln 10} = 19.5\ \text{s} = 0.33\ \text{min}$$
Proportional controller: $Q(s) = K_c\,\varepsilon(s)$, with $\varepsilon = T_R' - T_m'$ (Eqs. 8.24, 8.27).
Code 1/3 -- Eq. 8.10 from the balance¶
- Mirrors Derive 1/3: transformed balance, solved for $T'(s)$
sp.apartwith respect to $Q$ separates the two inputs
Qs, Tis, Ts = sp.symbols("Q_s T_is T_s") # transforms
w, Cp, V, rho_ = sp.symbols("w C V rho", positive=True)
bal = sp.Eq(Qs + w*Cp*(Tis - Ts), rho_*Cp*V*s*Ts) # Eq. 8.8
Tsol = sp.solve(bal, Ts)[0]
print(sp.collect(sp.expand(Tsol), [Qs, Tis]))
C*T_is*w/(C*V*rho*s + C*w) + Q_s/(C*V*rho*s + C*w)
Code 2/3 -- Example 8.1: blocks and superposition¶
- Mirrors Derive 2/3: two
ct.tfblocks, one per input - Responses to each input, and to both, at $t = 5$ min
G_Ti = ct.tf([1], [5, 1]) # T'/Ti'
G_Q = ct.tf([1 / 14], [5, 1]) # T'/Q, C per kW
tt = np.linspace(0, 30, 301)
a = ct.step_response(10 * G_Ti, tt).outputs # Ti +10 C
b = ct.step_response(42 * G_Q, tt).outputs # Q +42 kW
both = ct.forced_response(ct.tf([1], [5, 1]), tt,
10 + 42 / 14).outputs
print(f"t = 5 min: {a[50]:.2f} + {b[50]:.2f} = {both[50]:.2f} C")
t = 5 min: 6.32 + 1.90 = 8.22 C
Code 3/3 -- the thermocouple lags the tank (Ex. 8.3)¶
- Mirrors Derive 3/3: solve the 90% condition for $\tau_m$
ct.seriesconnects tank and sensor blocks
tm = float(sp.solve(sp.Eq(0.9, 1 - sp.exp(-45 / sp.Symbol("x"))),
sp.Symbol("x"))[0]) / 60 # min
G_m = ct.tf([1], [tm, 1])
Tm = ct.step_response(10 * ct.series(G_Ti, G_m), tt).outputs
print(f"tau_m = {tm:.3f} min; at t = 1 min: "
f"T' = {a[10]:.2f}, Tm' = {Tm[10]:.2f} C")
tau_m = 0.326 min; at t = 1 min: T' = 1.81, Tm' = 1.27 C
Build on it -- close the loop (Example 8.4)¶
- P controller $K_c = 20$ kW/C, set point +5 C
ct.feedback(forward, sensor)is the loop of Fig. 8-16
forward = ct.series(ct.tf([20], [1]), G_Q) # Kc then process
loop = ct.feedback(forward, G_m) # negative feedback
y = ct.step_response(5 * loop, tt).outputs
print(f"final T' = {y[-1]:.2f} C (asked for 5 C)")
fig, ax = plt.subplots(figsize=(7, 5.25))
ax.plot(tt, y); ax.axhline(5, ls="--", c="gray")
ax.set(xlabel="t (min)", ylabel="T' (C)"); plt.show()
final T' = 2.94 C (asked for 5 C)
Your Turn (8 min)¶
- By hand: in Example 8.4, at steady state the sensor reads the true temperature. Use $Q = K_c(T_R' - T')$ and $T' = Q/14$ to find the final $T'$ for $T_R' = 5$ C.
- Read the code: what does this print, to two decimals?
print(5 * ct.dcgain(loop))
Check -- cold-call¶
Example 8.4 changed the set point. If instead $T_i$ drops by 10 C with the set point fixed, is that a servo or a regulator problem? Which transfer function would you need?
Wrap-up¶
Derived, then coded: the manometer's $\tau$ and $\zeta$; the three damping cases; overshoot, decay ratio and period; the delay $e^{-\tau s}$ and its Pade approximations; the stirred-tank heater's blocks and the closed loop of Example 8.4.
Tools: ct.tf, ct.step_response, ct.step_info, ct.pade,
ct.series, ct.feedback, ct.dcgain, sp.series, curve_fit
Next: Ch. 12-14 -- closed-loop transients, offset, stability and root locus.