Agenda¶
Part A -- Interacting systems: the transfer-function matrix (Ch. 23, Sec. 23.1)
Part B -- How much interaction? Relative gains and pairing (Cecil, Sec. 7.3-7.5)
Part C -- Decoupling: cross-controllers and decouplers (Ch. 23; Cecil, Sec. 8.1)
Part D -- Stability of a multiloop system (Sec. 23.2)
Setup (run once)¶
- The standard imports
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 -- two inputs, two outputs (Fig. 23-2, Fig. 23-8)¶
- Two interacting tanks; flows $m_1$, $m_2$ in; levels $c_1$, $c_2$ out. Each input moves both levels.
- Ex. 23.1: $A_1 = 1$, $A_2 = \tfrac12$, $R_1 = \tfrac12$, $R_2 = 2$, $R_3 = 1$:
$$\dot c_1 = m_1 - 3c_1 + 2c_2,\qquad \dot c_2 = 2m_2 + 4c_1 - 5c_2$$
- Two loops ($m_1 \to c_1$, $m_2 \to c_2$): a set-point change in one disturbs the other through $G_{21}$, $G_{12}$.
Derive 1/2 -- the transfer-function matrix (Eq. 23.21)¶
$\dot{\mathbf c} = A\mathbf c + B\mathbf m$; transform with zero initial state:
$$\mathbf C(s) = (sI - A)^{-1}B\,\mathbf M(s) = G_p\mathbf M,\quad G_p = \frac{1}{(s+1)(s+7)}\begin{bmatrix} s + 5 & 4\\ 4 & 2(s + 3) \end{bmatrix}$$
Derive 2/2 -- the closed loop in matrix form (Eq. 23.11)¶
$\mathbf M = G_c\mathbf E$, $\mathbf E = \mathbf R - \mathbf C$ (valves, sensors = $I$):
$$\mathbf C = (I + G_pG_c)^{-1}G_pG_c\,\mathbf R$$
The scalar $G/(1 + G)$, with matrices. Order matters: $G_pG_c \ne G_cG_p$.
Code 1/3 -- $G_p$ with sympy¶
- Mirrors Derive 1/2: one matrix inverse
s = sp.symbols("s")
A = sp.Matrix([[-3, 2], [4, -5]])
B = sp.diag(1, 2)
Gp = sp.simplify((s * sp.eye(2) - A).inv() * B)
Gp.applyfunc(sp.factor)
Code 2/3 -- the same model, numerically¶
ct.sswith two inputs and two outputs; steady-state gains
An, Bn = np.array(A, dtype=float), np.array(B, dtype=float)
plant = ct.ss(An, Bn, np.eye(2), 0)
K = -np.linalg.inv(An) @ Bn # Gp(0)
print(np.round(K, 3)); print(np.round(ct.dcgain(plant), 3))
[[0.714 0.571] [0.571 0.857]] [[0.714 0.571] [0.571 0.857]]
Code 3/3 -- two P loops, step in $r_1$ (Fig. 23-14a)¶
- Mirrors Derive 2/2 for $G_c = \mathrm{diag}(4, 4)$
- With P control, $\mathbf m = K_c(\mathbf r - \mathbf c)$ folds into $A$
Kc = np.diag([4.0, 4.0])
loop = ct.ss(An - Bn @ Kc, Bn @ Kc, np.eye(2), 0)
t = np.linspace(0, 3, 301)
y = ct.step_response(loop, t).outputs # [output, input, time]
fig, ax = plt.subplots(figsize=(7, 5.25))
ax.plot(t, y[0, 0], label="c1"); ax.plot(t, y[1, 0], label="c2")
ax.set(xlabel="t", ylabel="response to r1 = 1"); ax.legend()
plt.show()
Your Turn (6 min)¶
- By hand: from Eq. 23.21, what is $G_{22}(0)$? What does it mean?
- Read the code: what is the shape of
y, and what isy[0, 1, -1]?
y = ct.step_response(loop, t).outputs
Check -- cold-call¶
Why can't we just tune loop 1 as if loop 2 did not exist?
Introduce -- one sensitivity, two values (Cecil, Fig. 7.7)¶
- Tune loop 1 with loop 2 on manual: process gain $K_{11} = \partial C_1/\partial M_1|_{M_2}$.
- Switch loop 2 to automatic: now $K'_{11} = \partial C_1/\partial M_1|_{C_2}$.
- Relative gain $\lambda_{11} = K_{11}/K'_{11}$: 1 means no interaction; 0.8-1.2 rarely a problem; 0.5-2 needs dynamic separation; negative: the loop changes direction.
Derive 1/2 -- $K'_{11}$ for a 2 x 2 process¶
Hold $\Delta C_2 = 0$ in $\Delta\mathbf C = K\Delta\mathbf M$:
$0 = K_{21}\Delta M_1 + K_{22}\Delta M_2 \Rightarrow \Delta M_2 = -\frac{K_{21}}{K_{22}}\Delta M_1$, so $K'_{11} = K_{11} - \frac{K_{12}K_{21}}{K_{22}}$ and
$$\lambda_{11} = \frac{K_{11}K_{22}}{K_{11}K_{22} - K_{12}K_{21}}$$
Derive 2/2 -- the relative gain array (Cecil approach 2)¶
In general $K'_{ij} = 1/(K^{-1})^T_{ij}$:
$$\Lambda = K \circ (K^{-1})^T$$
($\circ$: element by element.) Rows and columns sum to 1. Pair each $C_i$ with the $M_j$ whose $\lambda_{ij}$ is positive and closest to 1.
Code 1/3 -- the RGA as a function¶
- Mirrors Derive 2/2:
*on numpy arrays is element by element
def rga(K):
return K * np.linalg.inv(K).T
L = rga(K)
print(np.round(L, 3), "row sums:", L.sum(axis=1))
[[ 2.143 -1.143] [-1.143 2.143]] row sums: [1. 1.]
Code 2/3 -- check $K'_{11}$ by a "process test"¶
- Step $m_1$ with loop 2 on integral control; read $c_1$ (Cecil's Fig. 7.13 test)
def test(t, z): # z = [c1, c2, integral of c2]
c, I = z[:2], z[2]
m = np.array([1.0, -5.0 * I]) # m1 stepped; I-control on c2
return np.concatenate([An @ c + Bn @ m, [c[1]]])
sol = solve_ivp(test, (0, 20), np.zeros(3), max_step=0.01)
c1, c2 = sol.y[0, -1], sol.y[1, -1]
print(f"c2 = {c2:.1e}, K'11 = c1 = {c1:.4f}, "
f"lambda11 = {K[0, 0] / c1:.3f}")
c2 = 7.2e-10, K'11 = c1 = 0.3333, lambda11 = 2.143
Build on it -- Cecil's purified water process¶
- Gains from Cecil's steady-state model at zero user flow
- $C_1$ recirculation pressure, $C_2$ recirculation flow; $M_1$ valve, $M_2$ pump speed
Kw = np.array([[-0.546, 0.0444], # psig/%, psig/rpm
[1.160, 0.0109]]) # gpm/%, gpm/rpm
print(np.round(rga(Kw), 2))
Your Turn (6 min)¶
- By hand: $K = \begin{bmatrix}2 & 1\\ 1 & 2\end{bmatrix}$. Find $\lambda_{11}$ and the full RGA.
- Read the code: what does it print?
print(rga(np.diag([3.0, 0.5])))
Check -- poll¶
$\lambda_{11} = -0.6$ for your proposed pairing. Loop 1 is tuned with loop 2 on manual, then loop 2 is switched to automatic. Loop 1:
A) becomes slower B) becomes faster C) acts in the wrong direction
Introduce -- cancel the interaction (Fig. 23-5; Cecil Fig. 8.1)¶
- Add cross-controllers $G_{c12}$, $G_{c21}$: each loop's error also moves the other input, to cancel its side effect.
- Noninteracting if $G_o = G_pG_c$ is diagonal (Eq. 23.13).
- Cecil: a steady-state decoupler $D$ between controllers and process, $\mathbf m = D\mathbf x$ with $KD = I$, so $D = K^{-1}$.
Derive 1/2 -- the cross-controllers (Eqs. 23.14-23.15)¶
Set the off-diagonal elements of $G_pG_c$ to zero:
$$G_{c12} = -\frac{G_{12}G_{c22}}{G_{11}},\quad G_{c21} = -\frac{G_{21}G_{c11}}{G_{22}}$$
Ex. 23.2, $G_{c11} = K_1$, $G_{c22} = K_2$: $G_{c12} = \frac{-4K_2}{s + 5}$, $G_{c21} = \frac{-2K_1}{s + 3}$.
Derive 2/2 -- the decoupled loops (Eq. 23.25)¶
Substitute back:
$G_o = \mathrm{diag}\left(\frac{K_1}{s + 3}, \frac{2K_2}{s + 5}\right)$, so $\frac{C_1}{R_1} = \frac{K_1}{s + 3 + K_1}$ and $c_2$ does not move. With $K_1 = 4$: final $c_1 = 4/7 = 0.571$.
Code 1/3 -- Eqs. 23.14-23.15 with sympy¶
- Mirrors Derive 1/2; then check that $G_pG_c$ is diagonal
K1, K2 = sp.symbols("K1 K2", positive=True)
Gc12 = sp.simplify(-Gp[0, 1] * K2 / Gp[0, 0])
Gc21 = sp.simplify(-Gp[1, 0] * K1 / Gp[1, 1])
Go = sp.simplify(Gp * sp.Matrix([[K1, Gc12], [Gc21, K2]]))
print(Gc12, "|", Gc21); Go
-4*K2/(s + 5) | -2*K1/(s + 3)
Code 2/3 -- simulate with the cross-controllers (Fig. 23-14b)¶
- Each cross-controller is a first-order lag: one extra state each
def decoupled(t, z, r=np.array([1.0, 0.0]), k1=4.0, k2=4.0):
c, x = z[:2], z[2:] # levels, cross-ctrl states
e = r - c
m = np.array([k1*e[0] - 4*k2*x[0], k2*e[1] - 2*k1*x[1]])
return np.concatenate([An @ c + Bn @ m,
[-5*x[0] + e[1], -3*x[1] + e[0]]])
sol = solve_ivp(decoupled, (0, 3), np.zeros(4), t_eval=t,
max_step=0.01)
print(f"final c1 = {sol.y[0, -1]:.4f}, "
f"max |c2| = {np.abs(sol.y[1]).max():.1e}")
final c1 = 0.5714, max |c2| = 0.0e+00
Build on it -- Cecil's steady-state decoupler $D = K^{-1}$¶
- Two P controllers, $\mathbf m = D K_c(\mathbf r - \mathbf c)$
D = np.linalg.inv(K)
r = np.array([1.0, 0.0])
ss_dec = solve_ivp(lambda t, c: An @ c + Bn @ D @ Kc @ (r - c),
(0, 3), np.zeros(2), t_eval=t, max_step=0.01)
print(np.round(D, 2), "final:", np.round(ss_dec.y[:, -1], 3),
"peak c2:", round(np.abs(ss_dec.y[1]).max(), 3))
Your Turn (6 min)¶
- By hand: why does the decoupler need an accurate model? What happens to $c_2$ if the true $G_{12}$ is 20% larger than assumed?
- Read the code: what does this print?
print(np.round(K @ np.linalg.inv(K), 3))
Check -- think-pair-share¶
Cecil insists each decoupler output needs its own hand station and bumpless transfer. Why is "the decoupler as a whole on manual" not good enough?
Introduce -- one characteristic equation for all loops¶
- $\mathbf C = \dfrac{\mathrm{adj}(I + G_oG_m)\,G_o\mathbf R} {|I + G_oG_m|}$ (Eq. 23.28): every element has the same denominator.
- Characteristic equation: $|I + G_oG_m| = 0$ (Eq. 23.32).
- Ex. 23.4: Ex. 23.2 without cross-controllers. Stable for which $(K_1, K_2)$?
Derive 1/1 -- the determinant¶
$|I + G_pG_c| = (1 + K_1G_{11})(1 + K_2G_{22}) - K_1K_2G_{12}G_{21}$:
Over the common denominator $[(s+1)(s+7)]^2$ the book obtains a quartic (Eq. 23.34). Factor it: $(s+1)(s+7)$ cancels and
$$s^2 + (8 + K_1 + 2K_2)s + 7 + 5K_1 + 6K_2 + 2K_1K_2 = 0$$
Two states, static controllers: two closed-loop poles.
Code 1/2 -- the characteristic polynomial¶
- Mirrors Derive 1/1 with sympy
k1, k2 = sp.symbols("K1 K2")
det = sp.factor(sp.together((sp.eye(2) + Gp * sp.diag(k1, k2)).det()))
num, den = sp.fraction(det)
print("numerator:", sp.Poly(sp.expand(num), s).all_coeffs())
print("denominator:", den)
numerator: [1, K1 + 2*K2 + 8, 2*K1*K2 + 5*K1 + 6*K2 + 7] denominator: (s + 1)*(s + 7)
Code 2/2 -- the stability region in the $K_1$-$K_2$ plane¶
- Brute force on a grid with
np.roots; second order: all coefficients positive
coef = sp.lambdify((k1, k2), sp.Poly(sp.expand(num), s).all_coeffs())
ks = np.linspace(-10, 20, 151)
ok = np.array([[np.all(np.roots(np.array(coef(a, b), float)).real < 0)
for a in ks] for b in ks])
fig, ax = plt.subplots(figsize=(7, 5.25))
ax.contourf(ks, ks, ok, levels=[-0.5, 0.5, 1.5],
colors=["#f4cccc", "#d9ead3"])
ax.set(xlabel="K1", ylabel="K2", title="green: stable"); plt.show()
Your Turn (6 min)¶
- By hand: for a second-order polynomial $s^2 + a_1s + a_0$, what are the stability conditions? Apply them at $K_1 = K_2 = -2$.
- Read the code: what does this print?
print(np.all(np.roots([1.0, 2.0, -3.0]).real < 0))
Check -- cold-call¶
The book stops at "the Routh test can be applied" to the quartic. What did the factorization buy us, and when would it not be available?
Wrap-up¶
Derived, then coded: the transfer-function matrix $(sI - A)^{-1}B$; the closed loop in matrix form; relative gains and the RGA (checked by a simulated process test); cross-controllers and a steady-state decoupler; the multiloop characteristic equation and its stability region.
Tools: sp.Matrix.inv, ct.ss (MIMO), np.linalg.inv,
element-wise * versus @, solve_ivp with controller states,
sp.lambdify, contourf
Next: Ch. 24-25 -- nonlinear systems and the phase plane.