Skip to content

Instantly share code, notes, and snippets.

@raytroop
Last active May 24, 2026 09:36
Show Gist options
  • Select an option

  • Save raytroop/022b6cf1eb71dcb4667dc0bff110b413 to your computer and use it in GitHub Desktop.

Select an option

Save raytroop/022b6cf1eb71dcb4667dc0bff110b413 to your computer and use it in GitHub Desktop.
%% 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