Controlli automatici in Python - scipy.signal e sympy
In questa pagina 8
Il programma comprende "Applicazioni dei controlli automatici: SISO tool", cioè l'uso di un programma di calcolo per analizzare e sintetizzare controllori. Con Python si ottengono gli stessi risultati. Il codice qui sotto è stato eseguito e i numeri riportati sono quelli ottenuti. (I grafici si ottengono con matplotlib, per esempio plt.semilogx(w, 20*np.log10(abs(H))) per il modulo di Bode; non sono riprodotti.) L'uso più utile è la verifica di un progetto prima di consegnarlo: dopo avere scelto , si controlla la e il margine di fase effettivi.
Il sistema di esempio
import numpy as np
import sympy as sp
from scipy import signal, optimize
num = [10.0]
den = np.polymul(np.polymul([1, 0], [1, 1]), [0.1, 1]) # s (s+1) (0.1 s + 1)Poli dell'anello chiuso e luogo delle radici
I poli di sono le radici di (Luogo delle radici - definizione e regole di tracciamentoPer $G=\frac{n(s)}{d(s)}$ (monici, coprimi, $n=\deg d\ge m=\deg n$) e $W_K=\frac{KG}{1+KG}$, il luogo positivo è l'insieme dei $s$ con $d(s)+Kn(s)=0$ per qualche $K>0$ (i poli di $W_K$ al variare di $K$). Regole: $n$ rami simmetrici rispetto all'asse reale, partono dai poli ($K=0$) e finiscono negli $m$ zeri o negli $n-m$ asintoti; un punto reale è nel luogo positivo se alla sua destra ci sono un numero dispari di poli+zeri reali (con molteplicità); asintoti con direzioni $\frac{(2i+1)\pi}{n-m}$ e centro $x_B=\frac{\sum p_i-\sum z_i}{n-m}$; il guadagno in un punto è $K=-\frac{d(s)}{n(s)}$.Luogo delle radici - definizione e regole di tracciamento →): per ogni si calcolano con np.roots e il luogo è l'insieme di questi punti al variare di . Il valore critico si trova dove la massima parte reale si annulla:
poli = lambda K: np.roots(np.polyadd(den, K * np.array(num)))
for K in (0.1, 0.3, 1.1, 1.5):
print(f"K={K}: poli {np.round(poli(K), 3)}")
Kc = optimize.brentq(lambda K: poli(K).real.max(), 0.5, 2.0)
print("K critico =", round(Kc, 4))Risultato: : poli e ; : ; : e ; : (instabile); , uguale al limite di stabilità trovato con Routh e con Nyquist.
Pulsazione di attraversamento e margine di fase
si ottiene con signal.freqs(num, den, worN=[w]). è lo zero di (si cerca in scala logaritmica: il modulo varia di molti ordini di grandezza), e la fase di Bode si ricostruisce dai termini (qui ) per evitare il salto di di np.angle:
K0 = 0.1
H = lambda w: signal.freqs(K0 * np.array(num), den, worN=[w])[1][0]
wA = 10 ** optimize.brentq(lambda lw: np.log10(abs(H(10 ** lw))), -3, 3)
fase = -90 - np.degrees(np.arctan(wA)) - np.degrees(np.arctan(wA / 10))
print(f"wA = {wA:.3f} rad/s, margine di fase = {180 + fase:.1f} gradi")Risposta al gradino e parametri
signal.step(W, T=t) simula con condizioni iniziali nulle; con la definizione al 10% del corso (Risposta al gradino e suoi parametriPer $W$ strettamente propria, BIBO stabile e con $W(0)\ne0$ la risposta al gradino $w_{-1}$ parte da $0$ e tende a $W(0)$. I suoi parametri (al 10%) sono il tempo di salita $t_r=\min{t:|w_{-1}(t)-W(0)|\le0{,}1|W(0)|}$, il tempo di assestamento $t_s$ (da cui in poi la risposta resta nella banda del 10%) e la sovraelongazione $S=\frac{\max w_{-1}-W(0)}{W(0)}\cdot100%$. Se la risposta è monotona $t_r=t_s$ e $S=0$. Per $W=\frac K{s+p}$: $t_r=t_s=\frac{\ln10}p$. Uno zero instabile produce sottoelongazione (undershoot).Risposta al gradino e suoi parametri →):
W = signal.TransferFunction(K0 * np.array(num), np.polyadd(den, K0 * np.array(num)))
t = np.linspace(0, 80, 80001)
t, y = signal.step(W, T=t)
yf = y[-1]
tr = t[np.argmax(np.abs(y - yf) <= 0.1 * abs(yf))]
ts = t[np.where(np.abs(y - yf) > 0.1 * abs(yf))[0][-1] + 1]
print(f"tr = {tr:.2f} s, ts = {ts:.2f} s, S = {(y.max() - yf) / yf * 100:.1f} %")Risultato: s, s, . È coerente con la tabella di Margine di fase, banda e picco di risonanzaPer $G$ strettamente propria, senza poli instabili, con $K_B>0$ e $\omega_A$ ben definita, la banda di $W=\frac G{1+G}$ è molto vicina a $\omega_A$ ($B_p\approx\omega_A$) e il tempo di salita si stima con $t_r\approx\frac{2{,}3}{\omega_A}$. Il margine di fase controlla le oscillazioni: $|W(j\omega_A)|=\frac1{2\sin(m_\varphi/2)}$, che approssima il picco di risonanza; per $\frac{\omega_n^2}{s(s+2\xi\omega_n)}$ si ha $m_\varphi\approx100,\xi$ gradi. Specifiche tipiche: $m_\varphi\ge45^\circ$ per poche oscillazioni, $m_\varphi\approx90^\circ$ per risposta tipo primo ordine.Margine di fase, banda e picco di risonanza →: corrisponde a e . Nota: l'orizzonte di simulazione deve essere molto più lungo della costante di tempo più lenta; con poli (cioè ) servirebbero oltre s.
Diagramma di Nyquist
Il diagramma si ottiene valutando signal.freqs(num, den, worN=w) su una griglia fitta di (parte reale e immaginaria dei risultati; con matplotlib: plt.plot(H.real, H.imag)). L'intersezione con l'asse reale è dove la parte immaginaria cambia segno:
w = np.logspace(-2, 3, 200001)
Hn = signal.freqs(num, den, worN=w)[1]
i = np.where(np.diff(np.sign(Hn.imag)))[0][0]
print(f"asse reale in w = {w[i]:.3f}, punto {Hn.real[i]:.4f}")Routh con parametro (sympy)
sympy permette di tenere simbolico e ottenere direttamente gli intervalli di stabilità (Criterio di Routh-HurwitzIl criterio di Routh stabilisce se un polinomio $P(s)=p_ns^n+\dots+p_0$ è di Hurwitz senza calcolarne le radici. Si costruisce la tabella (righe $n,n-1,\dots,0$) con $\text{nuovo}=\frac{\text{pivot}\cdot a-\text{prec.}\cdot b}{\text{pivot}}$; se la tabella va a compimento, il numero di variazioni di segno nella prima colonna è il numero di radici con $\mathrm{Re}>0$, le permanenze il numero con $\mathrm{Re}<0$. Elemento nullo in prima colonna: $P$ non è Hurwitz; riga nulla: radici simmetriche (spesso immaginarie) date dal polinomio ausiliario. Con parametro $K$ nei coefficienti dà gli intervalli di stabilità.Criterio di Routh-Hurwitz →):
s, K = sp.symbols('s K')
c = sp.Poly(s * (1 + s) * (1 + s / 10) + 10 * K, s).all_coeffs() # [1/10, 11/10, 1, 10K]
prima_colonna = [c[0], c[1], sp.simplify((c[1] * c[2] - c[0] * c[3]) / c[1]), c[3]]
print(prima_colonna)
print(sp.solve_univariate_inequality(prima_colonna[2] > 0, K))Risultato: prima colonna ; condizione e , cioè .
Che cosa si può fare con la sola libreria standard
Con numpy e scipy si possono: fattorizzare polinomi (np.roots), costruire funzioni di trasferimento (signal.TransferFunction), calcolare risposte in frequenza (signal.freqs, per sistemi a tempo continuo), simulare (signal.step, signal.impulse, signal.lsim), moltiplicare e sommare polinomi (np.polymul, np.polyadd). Non c'è una funzione per luoghi delle radici o margini: si costruiscono come sopra. Con sympy si ottengono fratti semplici (sp.apart), trasformate (sp.laplace_transform) e limiti simbolici (sp.limit), utili per controllare i calcoli a mano.
Errori comuni
- Usare
np.angleper la fase senza srotolarla o ricostruirla: salta di e dà margini sbagliati. - Cercare con un intervallo lineare: il modulo varia di ordini di grandezza, serve la scala logaritmica.
- Simulare con un orizzonte troppo corto e dedurre un sbagliato.
- Fidarsi del numero invece del metodo: il risultato numerico serve a verificare il progetto, la giustificazione resta quella analitica richiesta nel compito.
Versione ripasso
- Polinomi come liste di coefficienti;
np.polymul,np.polyadd,np.roots; :den = polymul(polymul([1,0],[1,1]),[0.1,1]). - Luogo: poli di =
np.roots(np.polyadd(den, K*num))al variare di ; conoptimize.brentqsupoli(K).real.max(): (Luogo delle radici - definizione e regole di tracciamentoPer $G=\frac{n(s)}{d(s)}$ (monici, coprimi, $n=\deg d\ge m=\deg n$) e $W_K=\frac{KG}{1+KG}$, il luogo positivo è l'insieme dei $s$ con $d(s)+Kn(s)=0$ per qualche $K>0$ (i poli di $W_K$ al variare di $K$). Regole: $n$ rami simmetrici rispetto all'asse reale, partono dai poli ($K=0$) e finiscono negli $m$ zeri o negli $n-m$ asintoti; un punto reale è nel luogo positivo se alla sua destra ci sono un numero dispari di poli+zeri reali (con molteplicità); asintoti con direzioni $\frac{(2i+1)\pi}{n-m}$ e centro $x_B=\frac{\sum p_i-\sum z_i}{n-m}$; il guadagno in un punto è $K=-\frac{d(s)}{n(s)}$.Luogo delle radici - definizione e regole di tracciamento →). - e margine: zero di in scala log; fase di Bode ricostruita dai fattori; : , (Pulsazione di attraversamento, margine di fase e criterio di BodeLa pulsazione di attraversamento $\omega_A$ è la $\omega>0$ in cui $|G(j\omega)|=1$ (0 dB); il margine di fase è $m_\varphi=180^\circ+\arg G(j\omega_A)$: quanta fase si può ancora perdere prima di arrivare a $-180^\circ$. Criterio di Bode: se $G$ è strettamente propria, ha guadagno di Bode $K_B>0$, nessun polo con $\mathrm{Re}>0$ e $\omega_A$ è ben definita (esiste ed è unica), allora $W=\frac G{1+G}$ è BIBO stabile $\iff m_\varphi>0$. Più $m_\varphi$ è grande, più il sistema è lontano dall'instabilità e meno oscilla.Pulsazione di attraversamento, margine di fase e criterio di Bode →).
- Gradino:
signal.step; , al 10%, ; : s, s, . - Nyquist:
signal.freqssu griglia fitta; taglia l'asse reale in , . - Routh:
sympycon simbolico: (Criterio di Routh-HurwitzIl criterio di Routh stabilisce se un polinomio $P(s)=p_ns^n+\dots+p_0$ è di Hurwitz senza calcolarne le radici. Si costruisce la tabella (righe $n,n-1,\dots,0$) con $\text{nuovo}=\frac{\text{pivot}\cdot a-\text{prec.}\cdot b}{\text{pivot}}$; se la tabella va a compimento, il numero di variazioni di segno nella prima colonna è il numero di radici con $\mathrm{Re}>0$, le permanenze il numero con $\mathrm{Re}<0$. Elemento nullo in prima colonna: $P$ non è Hurwitz; riga nulla: radici simmetriche (spesso immaginarie) date dal polinomio ausiliario. Con parametro $K$ nei coefficienti dà gli intervalli di stabilità.Criterio di Routh-Hurwitz →). - Errori:
np.anglenon srotolata; con griglia lineare; orizzonte di simulazione corto; numero al posto del metodo.