Last active
May 24, 2026 09:36
-
-
Save raytroop/022b6cf1eb71dcb4667dc0bff110b413 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| %% Complex-conjugate poles of a 2nd-order system vs. quality factor Q | |
| % H(s) ~ 1 / ( s^2 + (w0/Q) s + w0^2 ) | |
| % Poles: s = -w0/(2Q) +/- w0*sqrt(1/(4Q^2) - 1) | |
| % For Q > 0.5 the poles are complex conjugates on a circle of radius w0. | |
| clear; clc; close all; | |
| w0 = 1.0; % natural frequency (normalized) | |
| % Discrete Q values to mark with 'x' | |
| Q_values = [0.5 0.55 0.6 0.707 0.8 1.0 1.5 2.0 3.0 5.0 10.0 50.0]; | |
| figure('Color','w','Position',[100 100 950 800]); | |
| hold on; box on; | |
| %% --- Circle of radius w0 (the pole locus) --- | |
| th = linspace(0,2*pi,400); | |
| plot(w0*cos(th), w0*sin(th), '--', 'Color',[.5 .5 .5], ... | |
| 'LineWidth',1, 'DisplayName','Circle of radius \omega_0'); | |
| %% --- Color map keyed to log10(Q) --- | |
| cmap = parula(256); | |
| lz = log10(Q_values); | |
| cnorm = (lz - min(lz)) / (max(lz) - min(lz)); % 0..1 | |
| %% --- Discrete poles for each Q --- | |
| for k = 1:numel(Q_values) | |
| Q = Q_values(k); | |
| sigma = -w0/(2*Q); % real part | |
| disc = 1/(4*Q^2) - 1; % negative for Q > 0.5 | |
| wd = w0*sqrt(-disc); % imaginary part magnitude | |
| ci = max(1, round(cnorm(k)*255)+1); | |
| col = cmap(ci,:); | |
| plot(sigma, wd, 'x', 'Color',col, 'MarkerSize',12, 'LineWidth',2.5, ... | |
| 'HandleVisibility','off'); | |
| plot(sigma, -wd, 'x', 'Color',col, 'MarkerSize',12, 'LineWidth',2.5, ... | |
| 'HandleVisibility','off'); | |
| end | |
| %% --- Continuous locus (upper & lower) --- | |
| Qf = linspace(0.5,200,2000); | |
| sf = -w0./(2*Qf); | |
| wdf = w0*sqrt(1 - 1./(4*Qf.^2)); | |
| plot(sf, wdf, '-', 'Color',[.27 .51 .71], 'LineWidth',1.3, 'HandleVisibility','off'); | |
| plot(sf, -wdf, '-', 'Color',[.27 .51 .71], 'LineWidth',1.3, 'HandleVisibility','off'); | |
| %% --- Axes through origin --- | |
| plot([-1.2 0.55],[0 0],'k-','LineWidth',0.8,'HandleVisibility','off'); | |
| plot([0 0],[-1.2 1.2],'k-','LineWidth',0.8,'HandleVisibility','off'); | |
| %% =================== ANNOTATIONS (pole Q labels) =================== | |
| % Each row: { Q, text (string or cellstr for multiline), tx, ty, h-align, v-align } | |
| % Positions chosen so each text box sits in clear space and its connector | |
| % emerges from the box edge facing the pole (driven by h/v alignment). | |
| annot = {0.5, {'Q = 0.5','(critical damping)'}, -1.00, -0.13, 'center', 'top'; | |
| 0.707, {'Q = 0.707','(Butterworth)'}, -0.85, 0.85, 'right', 'bottom'; | |
| 1.0, 'Q = 1', -0.72, 1.02, 'right', 'middle'; | |
| 2.0, 'Q = 2', -0.42, 1.13, 'right', 'middle'; | |
| 10.0, 'Q = 10', 0.12, 1.05, 'left', 'middle'; | |
| 50.0, {'Q = 50','(near j\omega axis)'}, 0.18, 0.82, 'left', 'middle'}; | |
| for k = 1:size(annot,1) | |
| Q = annot{k,1}; | |
| sigma = -w0/(2*Q); | |
| wd = w0*sqrt(max(1 - 1/(4*Q^2),0)); | |
| tx = annot{k,3}; ty = annot{k,4}; | |
| % connector first; text box covers the portion inside the label | |
| plot([tx sigma],[ty wd], '-', ... | |
| 'Color',[.4 .4 .4], 'LineWidth',0.8, 'HandleVisibility','off'); | |
| text(tx, ty, annot{k,2}, ... | |
| 'FontSize',8.5, ... | |
| 'HorizontalAlignment',annot{k,5}, ... | |
| 'VerticalAlignment',annot{k,6}, ... | |
| 'BackgroundColor',[1 1 .88], 'EdgeColor',[.5 .5 .5], 'Margin',3); | |
| end | |
| %% --- 45-degree reference line (verifies Butterworth pole sits on it) --- | |
| plot([0 -w0*cos(pi/4)],[0 w0*sin(pi/4)], ':', 'Color',[.2 .2 .2], ... | |
| 'LineWidth',1, 'DisplayName','45\circ reference (Butterworth)'); | |
| %% --- Radius line + angle for Q = 1 pole --- | |
| Qd = 1.0; sd = -w0/(2*Qd); wdd = w0*sqrt(1 - 1/(4*Qd^2)); | |
| plot([0 sd],[0 wdd], 'Color',[.86 .08 .24],'LineWidth',1.6,'HandleVisibility','off'); | |
| % |s|=w0 label sits on the inside of the radius line, away from other labels | |
| text(-0.32, 0.62, '|s| = \omega_0', 'Color',[.86 .08 .24], 'FontSize',10, ... | |
| 'HorizontalAlignment','center', 'VerticalAlignment','middle', ... | |
| 'Rotation', atan2(wdd, sd)*180/pi - 180, ... | |
| 'BackgroundColor','w', 'Margin',1); | |
| % angle arc from +imag axis down to the radius line | |
| ang = atan2(wdd, sd); % radians (measured from +x) | |
| aarc = linspace(pi/2, ang, 50); | |
| rr = 0.30; % smaller radius so it doesn't clash | |
| plot(rr*cos(aarc), rr*sin(aarc), 'Color',[.86 .08 .24],'LineWidth',1.5,'HandleVisibility','off'); | |
| % theta label sits just OUTSIDE the arc, near its mid-angle | |
| amid = (pi/2 + ang)/2; | |
| text(0.40*cos(amid)-0.02, 0.40*sin(amid)+0.02, '\theta = cos^{-1}(1/2Q)', ... | |
| 'Color',[.86 .08 .24], 'FontSize',10, ... | |
| 'HorizontalAlignment','center', 'VerticalAlignment','middle', ... | |
| 'BackgroundColor','w', 'Margin',1); | |
| %% --- increasing-Q arrow on the LOWER outer locus (clean area) --- | |
| % Tangent arrow along the locus; label placed on the OUTSIDE of the curve. | |
| Qa = 0.65; Qb = 0.95; | |
| xa = -w0/(2*Qa); ya = -w0*sqrt(1 - 1/(4*Qa^2)); | |
| xb = -w0/(2*Qb); yb = -w0*sqrt(1 - 1/(4*Qb^2)); | |
| quiver(xa, ya, xb-xa, yb-ya, 0, 'Color',[.27 .51 .71], ... | |
| 'LineWidth',2.5, 'MaxHeadSize',1.2, 'HandleVisibility','off'); | |
| % Label outside the circle near the arrow tail, rotated to follow the locus | |
| text(xa-0.06, ya-0.06, 'increasing Q', ... | |
| 'Color',[.27 .51 .71], 'FontSize',10, 'FontAngle','italic', ... | |
| 'HorizontalAlignment','right', 'VerticalAlignment','top', ... | |
| 'BackgroundColor','w', 'Margin',2); | |
| %% --- real/imag part labels for the Q = 2 pole --- | |
| Q2 = 2.0; s2 = -w0/(2*Q2); w2 = w0*sqrt(1 - 1/(4*Q2^2)); | |
| % sigma label sits INSIDE the circle so its connector stays clear of the | |
| % lower locus and the increasing-Q arrow | |
| text(-0.55, -0.50, '\sigma = -\omega_0/(2Q)', ... | |
| 'Color',[0 .39 0], 'FontSize',10, ... | |
| 'HorizontalAlignment','right', 'VerticalAlignment','middle', ... | |
| 'BackgroundColor','w', 'EdgeColor',[0 .39 0], 'Margin',2); | |
| plot([-0.55 s2],[-0.50 -w2], 'Color',[0 .39 0], 'LineWidth',1, 'HandleVisibility','off'); | |
| % omega_d label in lower-right, connector to LOWER Q=2 pole | |
| text(-0.05, -0.55, '$\omega_d = \omega_0\sqrt{1-1/(4Q^2)}$', ... | |
| 'Interpreter','latex', 'Color',[.5 0 .5], 'FontSize',11, ... | |
| 'HorizontalAlignment','left', 'VerticalAlignment','middle', ... | |
| 'BackgroundColor','w', 'EdgeColor',[.5 0 .5], 'Margin',2); | |
| plot([-0.05 s2],[-0.55 -w2], 'Color',[.5 0 .5], 'LineWidth',1, 'HandleVisibility','off'); | |
| %% --- marginal-stability note (clear lower-right corner) --- | |
| text(0.50, -0.20, {'As Q\rightarrow\infty:','poles approach j\omega axis','(undamped oscillation,','marginal stability)'}, ... | |
| 'FontSize',8.5, 'Color',[.7 .13 .13], ... | |
| 'HorizontalAlignment','right', 'VerticalAlignment','middle', ... | |
| 'BackgroundColor',[1 .9 .9], 'EdgeColor',[.7 .13 .13], 'Margin',3); | |
| %% --- stable region shading (LHP) --- | |
| patch([-1.2 0 0 -1.2],[-1.2 -1.2 1.2 1.2],[.6 .9 .6], ... | |
| 'FaceAlpha',0.05,'EdgeColor','none','HandleVisibility','off'); | |
| text(-1.1,-0.95, {'Left-half plane','(stable)'}, 'Color',[0 .5 0], ... | |
| 'FontSize',9,'FontAngle','italic'); | |
| %% --- formatting --- | |
| xlabel('Real axis \sigma (normalized by \omega_0)','FontSize',12); | |
| ylabel('Imaginary axis j\omega (normalized by \omega_0)','FontSize',12); | |
| title({'Complex-Conjugate Poles of a 2nd-Order System vs. Quality Factor Q', ... | |
| 'H(s) \propto 1 / ( s^2 + (\omega_0/Q) s + \omega_0^2 )'},'FontSize',13); | |
| xlim([-1.2 0.55]); ylim([-1.2 1.2]); | |
| grid on; set(gca,'GridAlpha',0.25); | |
| legend('Location','southeast'); | |
| %% --- colorbar for log10(Q) --- | |
| colormap(cmap); | |
| cb = colorbar; | |
| cb.Label.String = 'log_{10}(Q)'; | |
| caxis([min(lz) max(lz)]); | |
| % Lock equal data aspect AFTER the colorbar is created, so the circle | |
| % stays circular (colorbar steals figure width, not axis aspect). | |
| set(gca,'DataAspectRatio',[1 1 1]); | |
| %% --- save --- | |
| print(gcf,'poles_vs_Q','-dpng','-r150'); | |
| disp('saved poles_vs_Q.png'); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment