Três Exemplos Comentados, do QP Canônico ao QP Reduzido
Universidade Federal do Pará
Nota
Pré-requisito: DeePC — Introdução (Lema de Willems, formulação básica) e DeePC com Ruído (regularização de Tikhonov). Esta aula não repete essa teoria — ela mostra essa teoria em código.
DeePC resolve, a cada instante, um problema de controle preditivo usando diretamente uma trajetória de dados \((u_d, y_d)\) — sem nunca estimar \((A,B,C,D)\) — porque, pelo Lema Fundamental de Willems, essa trajetória já contém toda a informação sobre o comportamento do sistema (Coulson; Lygeros; Dörfler, 2019a; Willems et al., 2005).
Hoje, essa ideia vira código: em nenhum dos três exemplos a lei de controle lê as matrizes \((A,B,C)\) da planta — elas só existem para gerar os dados e simular a planta no laço fechado.
Os três exemplos de hoje implementam exatamente esses quatro passos — a diferença está em como cada passo é feito.
| Símbolo | Significado | Papel no código |
|---|---|---|
| \(T\) | amostras do experimento off-line | tamanho de ud,yd |
| \(T_{\text{ini}}\) | janela do “estado inicial” (\(\geq\) ordem \(n\) da planta) | linhas de Up,Yp |
| \(N\) | horizonte de predição | linhas de Uf,Yf |
| \(L=T_{\text{ini}}+N\) | profundidade da Hankel | hankel_mat(w, L) |
| \(Q,R\) | peso do erro de saída / esforço de controle | custo do QP |
| \(\lambda_g,\lambda_y\) | regularização de \(g\) / penalidade da folga \(\sigma_y\) | termos extras em \(H\) |
| \(u_{\min},u_{\max}\) | restrição de entrada | inequação do QP |
| \(g\) | combinação linear das colunas de Hankel | variável de decisão |
deepc_exemplo.m |
deepc_exemplo2.m |
deepc_exemplo_v3.m |
|
|---|---|---|---|
| Planta | 2ª ordem discreta simples | 2ª ordem contínua subamortecida (\(T_s=2\,\)ms) | a mesma do exemplo 2 |
| Variáveis de decisão | \(n_G=T-L+1\approx 182\) | \(n_G\approx 265\) | reduzido para \(r=2L\approx 72\) |
| Novidade | formulação canônica: folga \(\sigma_y\) + restrição de entrada | checagem formal de excitação persistente; mede \(t_{QP}\) vs. \(T_s\) | reparametriza \(g\) por SVD; elimina \(\sigma_y\) analiticamente |
| Pergunta que responde | “Como funciona o DeePC, de fato?” | “Isso roda em tempo real?” | “Como fazer rodar mais rápido?” |
Não são três técnicas diferentes — é o mesmo problema, refinado em três passadas: primeiro correto, depois realista, depois eficiente.
Os três .m originais usam quadprog (Optimization Toolbox). Aqui, reproduzimos a mesma matemática — mesmo \(H\), \(A_{eq}\), \(A_{in}\) — e resolvemos com scipy.optimize.minimize (QP genérico via SLSQP), só para executar ao vivo no slide sem depender de MATLAB. Os números vão ser próximos, não idênticos, aos dos scripts originais.
Uma planta de 2ª ordem, discreta, com ganho DC unitário — “desconhecida” no sentido de que só serve para gerar dados e simular a malha fechada:
Nota
A lei de controle do DeePC nunca vai usar A, B, C diretamente — só para gerar ud,yd (passo 1) e para simular x = A@x + B@u no laço fechado.
def hankel_mat(w, L):
w = np.atleast_2d(w)
if w.shape[0] == 1: w = w.T
Tt, q = w.shape
cols = Tt - L + 1
Hm = np.zeros((L*q, cols))
for j in range(cols):
Hm[:, j] = w[j:j+L, :].T.reshape(-1)
return Hm
ud = rng.standard_normal(T) # entrada aleatória (persistentemente excitante)
yd = np.zeros(T); x = np.zeros((2, 1))
for k in range(T):
yd[k] = (C @ x).item() + sig_n * rng.standard_normal()
x = A @ x + B * ud[k]Um sinal gaussiano i.i.d. já é, quase sempre, persistentemente excitante de qualquer ordem razoável — na prática, um PRBS longo cumpre o mesmo papel.
\[ H_L(w_d) = \begin{bmatrix} w_d(1) & \cdots & w_d(T-L+1) \\ \vdots & \ddots & \vdots \\ w_d(L) & \cdots & w_d(T) \end{bmatrix} \;=\; \begin{bmatrix} U_p \text{ ou } Y_p \\[2pt] U_f \text{ ou } Y_f \end{bmatrix} \]
g tem 182 componentes — uma por coluna das matrizes de Hankel
Cada coluna de \(H_L\) é um trecho de comprimento \(L\) da trajetória \((u_d,y_d)\); Up,Yp são suas primeiras \(T_{\text{ini}}\) linhas, Uf,Yf as últimas \(N\).
\[ \min_{g,\,\sigma_y} \; \underbrace{\|Y_fg - r\|_Q^2 + \|U_fg\|_R^2}_{\text{desempenho}} \;+\; \underbrace{\lambda_g\|g\|_2^2}_{\text{regularização}} \;+\; \underbrace{\lambda_y\|\sigma_y\|_2^2}_{\text{folga p/ ruído}} \] \[ \text{s.a.}\quad U_pg = u_{\text{ini}}, \qquad Y_pg - \sigma_y = y_{\text{ini}}, \qquad u_{\min}\leq U_fg \leq u_{\max} \]
Nota
Esta é a variante quadrática (\(\ell_2\)) da folga — mais simples de otimizar que a \(\ell_1\) mencionada em DeePC com Ruído (Coulson; Lygeros; Dörfler, 2019b). É exatamente o que deepc_exemplo.m implementa.
A variável de decisão é \(z=[g;\,\sigma_y]\). Cada bloco do QP tem um endereço exato no código:
H1 = 2*np.block([
[Q*(Yf.T@Yf) + R*(Uf.T@Uf) + lam_g*np.eye(nG), np.zeros((nG, Tini))],
[np.zeros((Tini, nG)), lam_y*np.eye(Tini)]
])
H1 = (H1 + H1.T)/2
Aeq1 = np.block([[Up, np.zeros((Tini, Tini))],
[Yp, -np.eye(Tini)]]) # Up g = u_ini ; Yp g - sigma_y = y_ini
Ain1 = np.block([[ Uf, np.zeros((N, Tini))],
[-Uf, np.zeros((N, Tini))]]) # u_min <= Uf g <= u_max
bin1 = np.concatenate([umax*np.ones(N), -umin*np.ones(N)])H1 carrega \(Q,R,\lambda_g,\lambda_y\); Aeq1 amarra \(g\) ao histórico recente; Ain1/bin1 impõem a restrição física de entrada.
from scipy.optimize import minimize
def resolve_qp(H, f, Aeq, beq, Ain, bin_, z0):
obj = lambda z: 0.5*z@H@z + f@z
grad = lambda z: H@z + f
cons = [{'type': 'eq', 'fun': lambda z: Aeq@z - beq, 'jac': lambda z: Aeq},
{'type': 'ineq', 'fun': lambda z: bin_ - Ain@z, 'jac': lambda z: -Ain}]
res = minimize(obj, z0, jac=grad, constraints=cons, method='SLSQP',
options={'maxiter': 300, 'ftol': 1e-10})
return res.x
Nsim = 80
ref = np.concatenate([np.ones(40), 0.5*np.ones(Nsim-40+N)]) # degrau 1 -> 0.5
x = np.zeros((2, 1))
u_ini, y_ini = np.zeros(Tini), np.zeros(Tini)
y_log1, u_log1 = np.zeros(Nsim), np.zeros(Nsim)
z0 = np.zeros(nG + Tini)
for k in range(Nsim):
r = ref[k:k+N]
f = np.concatenate([-2*Q*(Yf.T@r), np.zeros(Tini)])
beq = np.concatenate([u_ini, y_ini])
z0 = resolve_qp(H1, f, Aeq1, beq, Ain1, bin1, z0)
g = z0[:nG]
u = Uf[0, :] @ g # aplica só o 1º passo previsto
y = (C@x).item() + sig_n*rng.standard_normal()
x = A@x + B*u
u_log1[k], y_log1[k] = u, y
u_ini = np.concatenate([u_ini[1:], [u]]) # janela deslizante
y_ini = np.concatenate([y_ini[1:], [y]])u_ini,y_ini fazem o papel de estimador de estado implícito — é assim que o DeePC “sabe onde a planta está” sem nunca ter um modelo de estados.
import matplotlib.pyplot as plt
fig, axs = plt.subplots(2, 1, figsize=(9, 5.2), sharex=True)
axs[0].plot(y_log1, color=BLUE, lw=2, label="saída $y$")
axs[0].plot(ref[:Nsim], color=GRAY, lw=1.6, ls="--", label="referência $r$")
axs[0].set_ylabel("y"); axs[0].legend(loc="lower right"); axs[0].grid(alpha=.3)
axs[1].step(range(Nsim), u_log1, color=NAVY, lw=1.8, where="post", label="entrada $u$")
axs[1].axhline(umax, color=ORANGE, ls=":"); axs[1].axhline(umin, color=ORANGE, ls=":")
axs[1].set_ylabel("u"); axs[1].set_xlabel("passo $k$"); axs[1].legend(loc="lower right"); axs[1].grid(alpha=.3)
plt.tight_layout(); plt.show()
\[ \begin{bmatrix} A & B \\ 0 & 0 \end{bmatrix} = \exp\!\left(\begin{bmatrix} A_c & B_c \\ 0 & 0 \end{bmatrix} T_s\right) \]
from scipy.linalg import expm
rng = np.random.default_rng(1)
wn, zeta = 100.0, 0.3 # planta subamortecida (~16 Hz)
Ac = np.array([[0, 1], [-wn**2, -2*zeta*wn]])
Bc = np.array([[0], [wn**2]])
C = np.array([[1.0, 0.0]])
Ts = 2e-3 # período de amostragem [s]
n = 2
M = np.zeros((n+1, n+1)); M[:n, :n] = Ac; M[:n, n:] = Bc
Mz = expm(M*Ts)
A, B = Mz[:n, :n], Mz[:n, n:]
sig_n = 0.01O truque do bloco \(\exp\!\big([[A_c,B_c],[0,0]]T_s\big)\) discretiza \((A_c,B_c)\) por ZOH sem precisar da Control System Toolbox — é exatamente o que deepc_exemplo2.m faz em MATLAB.
T, Tini, N = 300, 6, 30 # (reduzidos aqui p/ renderizar rápido; original T=500,N=40)
L = Tini + N
Q, R = 100.0, 0.05
lam_g, lam_y = 30.0, 1e5
umin, umax = -1.5, 1.5
def hankel1d(w, L):
w = np.asarray(w).reshape(-1, 1); Tt = w.shape[0]; cols = Tt - L + 1
Hm = np.zeros((L, cols))
for j in range(cols): Hm[:, j] = w[j:j+L, 0]
return Hm
ud = rng.standard_normal(T); yd = np.zeros(T); x = np.zeros((n, 1))
for k in range(T):
yd[k] = (C@x).item() + sig_n*rng.standard_normal()
x = A@x + B*ud[k]
rk = np.linalg.matrix_rank(hankel1d(ud, L+n))
print(f"posto(Hankel(u, L+n)) = {rk} (esperado {L+n})")posto(Hankel(u, L+n)) = 38 (esperado 38)
\(u_d\) é persistentemente excitante de ordem \(L+n\) se \(H_{L+n}(u_d)\) tem posto linha completo — condição do Lema de Willems para que o Lema Fundamental se aplique (Willems et al., 2005).
nG = 265 (era 182 no Exemplo 1)
H2 = 2*np.block([
[Q*(Yf.T@Yf) + R*(Uf.T@Uf) + lam_g*np.eye(nG), np.zeros((nG, Tini))],
[np.zeros((Tini, nG)), lam_y*np.eye(Tini)]
])
H2 = (H2 + H2.T)/2
Aeq2 = np.block([[Up, np.zeros((Tini, Tini))], [Yp, -np.eye(Tini)]])
Ain2 = np.block([[ Uf, np.zeros((N, Tini))], [-Uf, np.zeros((N, Tini))]])
bin2 = np.concatenate([umax*np.ones(N), -umin*np.ones(N)])Mesma receita do Exemplo 1 — só os números mudaram (\(T,T_{\text{ini}},N,Q,R, \lambda_g\) maiores, para essa planta mais rápida e mais ruidosa em relação à escala de \(T_s\)).
import time
Nsim = 120
t = np.arange(Nsim+N)*Ts
ref = 1.0*(t<0.06) + 0.4*((t>=0.06)&(t<0.12)) + 0.8*((t>=0.12)&(t<0.18)) - 0.5*(t>=0.18)
x = np.zeros((n, 1))
u_ini, y_ini = np.zeros(Tini), np.zeros(Tini)
y_log2, u_log2, t_qp2 = np.zeros(Nsim), np.zeros(Nsim), np.zeros(Nsim)
z0 = np.zeros(nG+Tini)
for k in range(Nsim):
r = ref[k:k+N]
f = np.concatenate([-2*Q*(Yf.T@r), np.zeros(Tini)])
beq = np.concatenate([u_ini, y_ini])
t0 = time.perf_counter()
z0 = resolve_qp(H2, f, Aeq2, beq, Ain2, bin2, z0)
t_qp2[k] = time.perf_counter() - t0
g = z0[:nG]
u = Uf[0, :]@g
y = (C@x).item() + sig_n*rng.standard_normal()
x = A@x + B*u
u_log2[k], y_log2[k] = u, y
u_ini = np.concatenate([u_ini[1:], [u]])
y_ini = np.concatenate([y_ini[1:], [y]])fig, axs = plt.subplots(3, 1, figsize=(9, 6.6), sharex=True)
axs[0].plot(1e3*t[:Nsim], y_log2, color=BLUE, lw=1.8, label="saída $y$")
axs[0].plot(1e3*t[:Nsim], ref[:Nsim], color=GRAY, ls="--", lw=1.4, label="referência")
axs[0].set_ylabel("y"); axs[0].legend(loc="lower right"); axs[0].grid(alpha=.3)
axs[1].step(1e3*t[:Nsim], u_log2, color=NAVY, lw=1.6, where="post")
axs[1].axhline(umax, color=ORANGE, ls=":"); axs[1].axhline(umin, color=ORANGE, ls=":")
axs[1].set_ylabel("u"); axs[1].grid(alpha=.3)
axs[2].plot(1e3*t[1:Nsim], 1e3*t_qp2[1:], color=GRAY, lw=1.4, label="tempo do QP")
axs[2].axhline(1e3*Ts, color=ORANGE, ls="--", label="$T_s$")
axs[2].set_ylabel("$t_{QP}$ [ms]"); axs[2].set_xlabel("tempo [ms]")
axs[2].legend(loc="upper right"); axs[2].grid(alpha=.3)
plt.tight_layout(); plt.show()
RMS do erro: 0.1992
tempo médio do QP: 106.0 ms (Ts = 2.0 ms)
Aviso
O QP leva, em média, mais de cem vezes o período de amostragem \(T_s\) para resolver — nesta configuração, o controlador não roda em tempo real. Isso não é um detalhe: é o obstáculo prático mais citado contra o DeePC “puro” em plantas rápidas.
O tamanho do problema (\(n_G\approx 265\) variáveis) é o principal culpado — e é exatamente o que o Exemplo 3 ataca.
Olhe outra vez para o custo e as restrições do QP: \(g\) nunca aparece sozinho — só através de
\[ U_pg,\quad U_fg,\quad Y_pg,\quad Y_fg \quad\text{e}\quad \|g\|_2^2. \]
Empilhe essas quatro matrizes em \(H_z=[U_p;U_f;Y_p;Y_f]\). Qualquer componente de \(g\) no núcleo de \(H_z\) não muda nenhuma dessas quatro expressões — só aumenta \(\|g\|_2^2\), que é penalizado. Logo, a solução ótima \(g^\star\) está sempre no espaço-linha de \(H_z\).
Essa é a mesma observação que justifica deepc_exemplo_v3.m: o problema tem \(n_G=T-L+1\) variáveis, mas o subespaço que de fato importa é muito menor.
Seja \(H_z^\top = W S V^\top\) (SVD). As colunas de \(W\) associadas a valores singulares não-nulos formam uma base ortonormal do espaço-linha de \(H_z\). Escrevendo \(g=W\hat g\):
\[ \|g\|_2^2=\|\hat g\|_2^2, \qquad U_pg=(U_pW)\hat g, \qquad \text{etc.} \]
A dimensão reduzida é \(r=\operatorname{posto}(H_z)\leq 2L=2(T_{\text{ini}}+N)\) — não depende de \(T\). Colher mais dados off-line deixa \(g\) original maior, mas \(\hat g\) continua do mesmo tamanho.
A igualdade \(Y_pg-\sigma_y=y_{\text{ini}}\) é trivial de resolver: \(\sigma_y=Y_pg-y_{\text{ini}}\). Substituindo direto no custo \(\lambda_y\|\sigma_y\|_2^2\):
\[ \lambda_y\|Y_pg-y_{\text{ini}}\|_2^2 = \lambda_y g^\top(Y_p^\top Y_p)g \;-\;2\lambda_y\,y_{\text{ini}}^\top Y_pg \;+\; \text{cte.} \]
O termo quadrático engorda \(H\) (vira \(\lambda_y Y_p^\top Y_p\)), o termo linear engorda \(f\) — e \(\sigma_y\) desaparece como variável de decisão. Menos uma igualdade, menos \(T_{\text{ini}}\) variáveis, de graça.
Up, Uf, Yp, Yf = Up@W, Uf@W, Yp@W, Yf@W
nG_r = Up.shape[1]
H3 = 2*(Q*(Yf.T@Yf) + R*(Uf.T@Uf) + lam_g*np.eye(nG_r) + lam_y*(Yp.T@Yp))
H3 = (H3 + H3.T)/2
Aeq3 = Up # só isto: Up g = u_ini
Ain3 = np.vstack([Uf, -Uf])
bin3 = np.concatenate([umax*np.ones(N), -umin*np.ones(N)])
print(f"QP original : {nG+Tini} variáveis, {2*Tini} igualdades")QP original : 271 variáveis, 12 igualdades
QP reduzido : 72 variáveis, 6 igualdade(s)
Compare com o Exemplo 2: Aeq2 tinha \(2T_{\text{ini}}\) linhas e \(n_G+T_{\text{ini}}\) colunas; Aeq3 é só Up — uma matriz \(T_{\text{ini}}\times r\).
rng = np.random.default_rng(1)
ud_ = rng.standard_normal(T); yd_ = np.zeros(T); x = np.zeros((n, 1))
for k in range(T): # mesma coleta offline do Exemplo 2
yd_[k] = (C@x).item() + sig_n*rng.standard_normal()
x = A@x + B*ud_[k]
x = np.zeros((n, 1))
u_ini, y_ini = np.zeros(Tini), np.zeros(Tini)
y_log3, u_log3, t_qp3 = np.zeros(Nsim), np.zeros(Nsim), np.zeros(Nsim)
z0 = np.zeros(nG_r)
for k in range(Nsim):
r_k = ref[k:k+N]
f = -2*(Q*(Yf.T@r_k) + lam_y*(Yp.T@y_ini))
beq = u_ini
t0 = time.perf_counter()
g = resolve_qp(H3, f, Aeq3, beq, Ain3, bin3, z0)
t_qp3[k] = time.perf_counter() - t0
z0 = g
u = Uf[0, :]@g
y = (C@x).item() + sig_n*rng.standard_normal()
x = A@x + B*u
u_log3[k], y_log3[k] = u, y
u_ini = np.concatenate([u_ini[1:], [u]])
y_ini = np.concatenate([y_ini[1:], [y]])[<matplotlib.lines.Line2D object at 0x13b508410>]
[<matplotlib.lines.Line2D object at 0x13b508550>]
Text(0, 0.5, 'y')
<matplotlib.legend.Legend object at 0x13b508690>
[<matplotlib.lines.Line2D object at 0x13b509bd0>]
<matplotlib.lines.Line2D object at 0x13b509d10>
<matplotlib.lines.Line2D object at 0x13b509e50>
Text(0, 0.5, 'u')
[<matplotlib.lines.Line2D object at 0x13b3ee350>]
<matplotlib.lines.Line2D object at 0x13b3ee990>
Text(0, 0.5, '$t_{QP}$ [ms]')
Text(0.5, 0, 'tempo [ms]')
<matplotlib.legend.Legend object at 0x13b3eead0>

RMS do erro: 0.1992 (Exemplo 2: confira o slide anterior)
Text(0, 0.5, 'variáveis de decisão')
[Text(0, 0, '271'), Text(0, 0, '72')]
Text(0, 0.5, 'tempo médio do QP [ms]')
<matplotlib.lines.Line2D object at 0x13b6287d0>
[Text(0, 0, '106.0'), Text(0, 0, '17.6')]

scipy+SLSQP é um solver genérico. Um quadprog/OSQP/qpOASES dedicado, como o MATLAB usa, fecharia boa parte dessa distância;deepc_exemplo_v3.m) reduz ainda mais o QP e funciona como denoising — a mesma ideia de regularização de DeePC com Ruído, agora vista pelo ângulo da compressão de \(g\).| Exemplo 1 | Exemplo 2 | Exemplo 3 | |
|---|---|---|---|
| Pergunta | Como o DeePC funciona? | Isso roda em tempo real? | Como fazer rodar mais rápido? |
| Planta | 2ª ordem discreta | 2ª ordem contínua, \(T_s=2\,\)ms | a mesma do Exemplo 2 |
| Variável de decisão | \(z=[g;\sigma_y]\), \(n_G+T_{\text{ini}}\approx 186\) | idem, \(\approx 271\) | \(\hat g\), \(r=2L\approx 72\) |
| O que aprendemos | receita completa: dados → Hankel → QP → horizonte deslizante | \(t_{QP}\) pode violar \(T_s\): viabilidade não é automática | mesma solução, QP muito mais barato via SVD |
deepc_exemplo.m, deepc_exemplo2.m, deepc_exemplo_v3.m.Prof. Dr. Raphael Teixeira
Universidade Federal do Pará