0% found this document useful (0 votes)
2 views

writeup-6

The document presents Control CHW No. 6, focusing on frequency response analysis for two transfer functions, G1 and G2, with specified parameters. It includes various plots such as Bode, Nyquist, and Nichols, along with code snippets for calculating and visualizing the frequency responses. The author expresses a preference against using Simulink and emphasizes the analytical and graphical methods used in the analysis.
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
2 views

writeup-6

The document presents Control CHW No. 6, focusing on frequency response analysis for two transfer functions, G1 and G2, with specified parameters. It includes various plots such as Bode, Nyquist, and Nichols, along with code snippets for calculating and visualizing the frequency responses. The author expresses a preference against using Simulink and emphasizes the analytical and graphical methods used in the analysis.
Copyright
© © All Rights Reserved
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 24

Control CHW No.

6
Frequency Response
2024-12-17

Erfan Khadem
Sharif University of Technology
Autumn 2024
Linear Control Systems
By Dr. Ahi
Control CHW No. 6 Erfan Khadem

Table of contents
Note .............................................................................................................................................................. 3
I. Charts and Answers .............................................................................................................................. 3
I.1. Problem No. 6 ...................................................................................................................................... 3
Note .............................................................................................................................................................. 3
Code Output ............................................................................................................................................... 3
Explanation .............................................................................................................................................. 10
Step 3 .......................................................................................................................................................... 10
Step 7 .......................................................................................................................................................... 10
Code ........................................................................................................................................................... 10
I.2. Problem No. 7 .................................................................................................................................... 18
Code Output ............................................................................................................................................. 18
Explanation .............................................................................................................................................. 20
Code ........................................................................................................................................................... 20

2 / 24
Control CHW No. 6 Erfan Khadem

Note
AI Assistance was not used in completing this assignment.
Also, I would like to express my deep gratitude for not including any simulink exercises. I
hate that utility with a passion.

I. Charts and Answers

I.1. Problem No. 6


Note
Unless otherwise mentioned, assume 𝜁 = 0.05 and 𝜔𝑛 = 10.0 rad
𝑠

Code Output

Fig. 1 — Frequency Response of 𝐺1 and 𝐺2 , Linear Frequency Axis, with 𝜔𝑟

3 / 24
Control CHW No. 6 Erfan Khadem

Fig. 2 — Manual Bode Plot of 𝐺1 and 𝐺2 , with 𝑀𝑝

Fig. 3 — Native Bode Plot of 𝐺1 and 𝐺2 , with 𝜑𝑚 ≈ 64.9° at 𝜔𝑐 ≈ 17.29 rad


𝑠 , Agrees with Fig. 2

4 / 24
Control CHW No. 6 Erfan Khadem

Fig. 4 — Surface Plot of 𝑀𝑝 vs. 𝜁 and 𝜔𝑛

𝑀𝑝 (𝐺1 )
Fig. 5 — Surface Plot of the ratios of 𝑀𝑝 (𝐺2 ) vs. 𝜁 and 𝜔𝑛

5 / 24
Control CHW No. 6 Erfan Khadem

Fig. 6 — Nyquist Plot of 𝐺2

6 / 24
Control CHW No. 6 Erfan Khadem

Fig. 7 — Manual Nichols Plot of 𝐺2

Fig. 8 — Native Nichols Plot of 𝐺2

7 / 24
Control CHW No. 6 Erfan Khadem

Fig. 9 — Step Response of 𝐺2

8 / 24
Control CHW No. 6 Erfan Khadem

Fig. 10 — Analytical and Text Output, Part 1

9 / 24
Control CHW No. 6 Erfan Khadem

Fig. 11 — Analytical and Text Output, Part 2

Explanation

Step 3
Due to the fact that 𝜁 ≪ 1, we can see that strong peaking behavior happens around 𝑠 ≈
𝑗𝜔𝑛 . Since both transfer functions have the same denominator, and their respective peaking
frequency are close, we can see that:

𝐺2,max 𝐺 (𝑗𝜔 ) 𝜔 (𝑗𝜔 + 𝜔𝑛 ) √


| | ≈ | 2 𝑛 | = | 𝑛 𝑛2 |= 2
𝐺1,max 𝐺1 (𝑗𝜔𝑛 ) 𝜔𝑛

Step 7
We can see that by increasing 𝜁, the overshoot percentage drops drastically (as demonstrated
in Fig. 4).

Code
import numpy as np
import control
import matplotlib.pyplot as plt
from scipy.signal import TransferFunction, bode, freqresp, step
from sympy import symbols, Function, I, simplify, diff, sqrt
import sympy as sp
import pprint

10 / 24
Control CHW No. 6 Erfan Khadem

from mpl_toolkits.mplot3d import Axes3D

wn = 10.0
zeta = 0.05

# Transfer functions G1 and G2:


# G1(s) = wn^2 / (s^2 + 2*zeta*wn*s + wn^2)
num_G1 = [wn**2]
den = [1, 2*zeta*wn, wn**2]

G1 = TransferFunction(num_G1, den)

# G2(s) = wn*(s + wn) / (s^2 + 2*zeta*wn*s + wn^2)


num_G2 = [wn, wn**2]
G2 = TransferFunction(num_G2, den)

# -------------------------------------
# Calculate Maximum Gain
# -------------------------------------
w = np.linspace(0, 5*wn, 1000)

_, G1_resp = freqresp(G1, w)
_, G2_resp = freqresp(G2, w)

G1_mag = np.abs(G1_resp)
G2_mag = np.abs(G2_resp)

G1_max = np.max(G1_mag)
G2_max = np.max(G2_mag)
G1_max_freq = w[np.argmax(G1_mag)]
G2_max_freq = w[np.argmax(G2_mag)]

# Check the ratio for small zeta:


ratio = G1_max / (G2_max / np.sqrt(2))

plt.figure(figsize=(10,6))
plt.plot(w, 20*np.log10(G1_mag), label='|G1(jw)|')
plt.plot(w, 20*np.log10(G2_mag), label='|G2(jw)|', linestyle='--')
plt.xlabel('Frequency (rad/s)')
plt.ylabel('Magnitude (dB)')
plt.title('Frequency Response of G1 and G2')
plt.grid(True)
plt.axvline(G1_max_freq, color='r', linestyle=':', label=f'G1 max at
{G1_max_freq:.2f} rad/s')
plt.axvline(G2_max_freq, color='g', linestyle=':', label=f'G2 max at
{G2_max_freq:.2f} rad/s')
plt.legend()
plt.show()

print('-' * 30)
print(' ' * 30)

11 / 24
Control CHW No. 6 Erfan Khadem

print("G1 max gain:", G1_max)


print("G1 max gain frequency:", G1_max_freq)
print(' ' * 30)
print('-' * 30)
print(' ' * 30)
print("G2 max gain:", G2_max)
print("G2 max gain frequency:", G2_max_freq)
print(' ' * 30)
print('-' * 30)
print(' ' * 30)
print("Approx. ratio G1_max/(G2_max/sqrt(2)):", ratio)
print(' ' * 30)
print('-' * 30)
print(' ' * 30)

# For explanation see `step 3` above

# -------------------------------------
# Analytical Check with sympy
# -------------------------------------
s = symbols('s', complex=True)
omega = symbols('omega', real=True, positive=True)
z, w_n = symbols('zeta omega_n', real=True, positive=True)

G1_sym = (w_n**2) / (s**2 + 2*z*w_n*s + w_n**2)


G2_sym = w_n*(s + w_n) / (s**2 + 2*z*w_n*s + w_n**2)

# Substitute s = j*omega and analyze magnitude


G1_jw = G1_sym.subs(s, 1j*omega)
G2_jw = G2_sym.subs(s, 1j*omega)

G1_mag_sq = sp.simplify(sp.Abs(G1_jw)**2)
G2_mag_sq = sp.simplify(sp.Abs(G2_jw)**2)

# Differentiate and solve for critical points:


dG1 = diff(G1_mag_sq, omega)
dG2 = diff(G2_mag_sq, omega)

G1_crit_points = sp.solve(sp.Eq(dG1, 0), omega, dict=True)


G2_crit_points = sp.solve(sp.Eq(dG2, 0), omega, dict=True)

# Typically, the maximum occurs around omega ~ w_n for small zeta. Let's just
confirm:
print('-' * 30)
print("G1 critical points:")
print(' ' * 30)
sp.pprint(G1_crit_points)
print('-' * 30)
print(' ' * 30)
print('-' * 30)
print("G2 critical points:")

12 / 24
Control CHW No. 6 Erfan Khadem

print(' ' * 30)


sp.pprint(G2_crit_points)
print('-' * 30)

eval_points = {z: zeta, w_n: wn}


G1_omega_r = G1_crit_points[0][omega].evalf(subs=eval_points)
G1_peak = (G1_mag_sq.subs({omega: G1_omega_r, z:zeta, w_n:wn}))**0.5

# Note: one of the G2_crit_points is at an imaginary frequency, we should


ignore it
for point in G2_crit_points:
G2_omega_r = point[omega].evalf(subs=eval_points)
if not type(G2_omega_r) is sp.core.numbers.Float: # Ignore the complex one
continue
print(' ' * 30)
print('-' * 30)
print(' ' * 30)
print("Found the critical point:")
sp.pprint(point)
print(' ' * 30)
print('-' * 30)
print(' ' * 30)
break

G2_peak = (G2_mag_sq.subs({omega: G2_omega_r, z:zeta, w_n:wn}))**0.5

print(' ' * 30)


print('-' * 30)
print(' ' * 30)
print("Analytical G1 peak magnitude:", (G1_peak))
print("Analytical G2 peak magnitude:", (G2_peak))

if G1_peak and G2_peak:


print("Analytical Ratio G1_peak / (G2_peak/sqrt(2)):", float(G1_peak/
(G2_peak/sp.sqrt(2))))

print(' ' * 30)


print('-' * 30)
print(' ' * 30)
# -------------------------------------
# 3. Bode Plot
# -------------------------------------
# Using scipy.signal.bode. This is just a glorified tf evaluation
# Could have been a simple `abs`, `atan2` and `sympy.lambdify` for numpy
w_bode, mag_G1, phase_G1 = bode(G1, w=w)
w_bode, mag_G2, phase_G2 = bode(G2, w=w)

# mag_G1, mag_G2 are in dB


G1_max_dB = np.max(mag_G1)
G2_max_dB = np.max(mag_G2)
G1_max_idx = np.argmax(mag_G1)
G2_max_idx = np.argmax(mag_G2)

13 / 24
Control CHW No. 6 Erfan Khadem

plt.figure(figsize=(10,8))
plt.subplot(2,1,1)
plt.semilogx(w_bode, mag_G1, label='G1')
plt.semilogx(w_bode, mag_G2, label='G2', linestyle='--')
plt.grid(True, which='both')
plt.ylabel('Magnitude (dB)')
plt.title('Bode Plot: Magnitude')
plt.plot(w_bode[G1_max_idx], G1_max_dB, 'ro', label=f"G1 peak = {G1_max_dB:.2f}
dB")
plt.plot(w_bode[G2_max_idx], G2_max_dB, 'go', label=f"G2 peak = {G2_max_dB:.2f}
dB")
plt.legend()

plt.subplot(2,1,2)
plt.semilogx(w_bode, phase_G1, label='G1')
plt.semilogx(w_bode, phase_G2, label='G2', linestyle='--')
plt.grid(True, which='both')
plt.xlabel('Frequency (rad/s)')
plt.ylabel('Phase (deg)')
plt.title('Bode Plot: Phase')
plt.legend()
plt.tight_layout()
plt.show()

# Plot using an existing library for verification


cplt = control.bode_plot(control.TransferFunction(num_G2, den), omega=w_bode,
display_margins=True, title="Bode Plot Using Control Library", grid=True,
dB=True)
cplt.figure.axes[0].grid(True)
cplt.figure.axes[1].grid(True)
plt.show()

# 3D plots w.r.t zeta, wn, and |G|


# For demonstration, we vary zeta and wn and compute peak gain of G1 and G2:
# Note: wn should not influence M_p
zeta_values = np.linspace(0.01, 0.3, 100)
wn_values = np.linspace(5, 20, 100)
G1_peaks = np.zeros((len(zeta_values), len(wn_values)))
G2_peaks = np.zeros((len(zeta_values), len(wn_values)))

for i, zz in enumerate(zeta_values):
for j, wnn in enumerate(wn_values):
G1_temp = TransferFunction([wnn**2],[1,2*zz*wnn,wnn**2])
G2_temp = TransferFunction([wnn, wnn**2],[1,2*zz*wnn,wnn**2])
# Compute frequency response
w_temp = np.linspace(0,5*wnn,500)
_, G1_resp_temp = freqresp(G1_temp, w_temp)
_, G2_resp_temp = freqresp(G2_temp, w_temp)
G1_peaks[i,j] = np.max(np.abs(G1_resp_temp))
G2_peaks[i,j] = np.max(np.abs(G2_resp_temp))

14 / 24
Control CHW No. 6 Erfan Khadem

ZETA, WNN = np.meshgrid(zeta_values, wn_values)

fig = plt.figure(figsize=(12,5))
ax1 = fig.add_subplot(1,2,1, projection='3d')
ax2 = fig.add_subplot(1,2,2, projection='3d')

surf1 = ax1.plot_surface(ZETA, WNN, G1_peaks.T, cmap='viridis')


ax1.set_xlabel('Zeta')
ax1.set_ylabel('Wn')
ax1.set_zlabel('|G1| max')
ax1.set_title('Max |G1| vs Zeta, Wn')
fig.colorbar(surf1, ax=ax1, shrink=0.5)

surf2 = ax2.plot_surface(ZETA, WNN, G2_peaks.T, cmap='viridis')


ax2.set_xlabel('Zeta')
ax2.set_ylabel('Wn')
ax2.set_zlabel('|G2| max')
ax2.set_title('Max |G2| vs Zeta, Wn')
fig.colorbar(surf2, ax=ax2, shrink=0.5)

plt.show()

fig = plt.figure()
ax1 = fig.add_subplot(1,1,1, projection='3d')
surf_ratio = ax1.plot_surface(ZETA, WNN, G1_peaks.T / G2_peaks.T,
cmap='viridis')
ax1.set_xlabel('Zeta')
ax1.set_ylabel('Wn')
ax1.set_zlabel(r'$\frac{|G1| max}{|G2| max}$')
ax1.set_title('Max |G1| / Max |G2| vs Zeta, Wn')
fig.colorbar(surf_ratio, ax=ax1)

plt.show()

# -------------------------------------
# 4. Nyquist and Nichols Plot for G2(s)
# -------------------------------------
# Nyquist plot:

fig = plt.figure(figsize=(6,6))
# :star: :star:
control.nyquist_plot(control.TransferFunction(num_G2,
den), omega=np.linspace(0,5*wn,2000))
plt.title('Nyquist plot of G2')
plt.show()

# Nichols plot:
# Nichols plot is basically magnitude (dB) vs phase. We can do it manually:
w_nich = np.logspace(-1, 2, 2000)
mag_nich, phase_nich, _ =
control.frequency_response(control.TransferFunction(num_G2, den), omega=w_nich)
mag_dB_nich = 20*np.log10(np.abs(mag_nich))

15 / 24
Control CHW No. 6 Erfan Khadem

plt.figure()
plt.plot(phase_nich*180/np.pi, mag_dB_nich)
plt.title('Manual Nichols plot of G2')
plt.xlabel('Phase (deg)')
plt.ylabel('Gain (dB)')
plt.grid(True)
plt.show()

control.nichols_plot(control.TransferFunction(num_G2, den), omega=w_nich,


grid=False)
# Use a custom nicholes plot. These lines were hardcoded manually.
control.nichols_grid(
cl_mags=np.array([-20, -10, -5, -2.5, -1, -0.5, 0, 0.25, 0.5, 1, 2, 3, 6,
12], dtype=np.float64),
cl_phases=np.array([-359, -330, -270, -210, -180, -120, -90, -70, -50, -30,
-10, -1], dtype=np.float64)
)
#plt.grid(True)
plt.title('Nichols plot of G2')
plt.show()

# Comments on Nyquist/Nichols:
# Nyquist plot: The plot of G2(jw) in the complex plane. For low damping, the
trajectory comes close
# to the negative real axis, indicating the system's resonant nature. The Nyquist
plot helps analyze
# stability through encirclements of the -1 point.
#
# Nichols plot: Plots magnitude (dB) vs phase (deg). Useful in loop shaping.
Shows how the gain and
# phase relate without frequency explicitly. A pronounced peak near resonance
frequency is visible.
# Can be used (with the grid) to find the closed loop response.

# -------------------------------------
# 5. Performance Indices of G2(s)
# -------------------------------------
G2_ctrl = control.TransferFunction(num_G2, den)
GM, PM, Wcg, Wcp = control.margin(G2_ctrl)
print(' ' * 30)
print('-' * 30)
print(' ' * 30)

print("Gain Margin (dB):", 20*np.log10(GM) if GM is not None else None)


print("Phase Margin (deg):", PM)
print("Crossover frequencies Wcg (gain), Wcp (phase):", Wcg, Wcp)

print(' ' * 30)


print('-' * 30)
print(' ' * 30)

G2_step_info = control.step_info(G2_ctrl)

16 / 24
Control CHW No. 6 Erfan Khadem

print("Step Response Information for G2:")


pprint.pprint(G2_step_info)

print(' ' * 30)


print('-' * 30)
print(' ' * 30)

# Bandwidth: frequency at which |G2(jw)| drops 3 dB below low-frequency gain.


# Low frequency gain of G2(s): When s->0, G2(0)= wn*(0+wn)/(wn^2)=1, so 0 dB
low-frequency gain.
# Find freq where magnitude is -3dB:
three_db_point = np.argmin(np.abs(mag_G2 + 3)) # since mag_G2 is in dB
BW = w_bode[three_db_point]
print("Bandwidth approx (rad/s):", BW)
# Note: BW should be approx. 20 rad/sec. OpenLoop G_2 hits 0 dB with phase =
-120 degree at ~17rad/sec.

# Peak Gain in dB:


PeakGain_dB = np.max(mag_G2)
print("Peak Gain (dB):", PeakGain_dB)

print(' ' * 30)


print('-' * 30)
print(' ' * 30)

# Step response of G2:


t, y = control.step_response(G2_ctrl)
plt.figure()
plt.plot(t, y)
plt.title('Step response of G2(s)')
plt.xlabel('Time (s)')
plt.ylabel('Response')
plt.grid(True)
plt.show()

# Summary w.r.t zeta:


# - As zeta increases, damping increases, the resonance peak decreases, and the
system becomes less oscillatory.
# - For small zeta, the system exhibits a large resonant peak and more oscillatory
behavior.
# - As zeta -> 0, G1 and G2 peaks become very large, and the ratio of their
peaks approaches sqrt(2).

17 / 24
Control CHW No. 6 Erfan Khadem

I.2. Problem No. 7


Code Output

Fig. 12 — 50 dB Attenuation Bandwidth vs. 𝜁

Fig. 13 — Attenuation vs. Frequency

18 / 24
Control CHW No. 6 Erfan Khadem

Fig. 14 — Attenuation vs. Frequency (Zoomed Version)

Fig. 15 — Time Domain Output, Calculated Using FFT

19 / 24
Control CHW No. 6 Erfan Khadem

Fig. 16 — Time Domain Output, Zero Initial Conditions

Fig. 17 — Frequency Domain Output

Explanation
By tuning 𝜁 we are effectively setting the quality factor of our filter. As we make 𝜁 larger, the
effective stop band increases linearly, as witnessed by Fig. 12. The parameter 𝜔𝑛 determines
the notch frequency, and should be set to 2 × 𝜋 × 50 rad𝑠 .

Code
import numpy as np
import matplotlib.pyplot as plt
from scipy.signal import freqresp, lfilter, TransferFunction

import control

20 / 24
Control CHW No. 6 Erfan Khadem

f0 = 50.0
omega_n_notch = 2*np.pi*f0
zeta_values = np.linspace(5, 7, 500)

def notch_filter(omega_n, zeta, for_control=False):


num = [1, 0, omega_n**2]
den = [1, 2*zeta*omega_n, omega_n**2]
if for_control:
return control.TransferFunction(num, den)
return TransferFunction(num, den)

# Plot 50 dB bandwidth vs various zeta


BW_values = []
w_test = np.linspace(40 * 2 * np.pi, 60 * 2 * np.pi, 100000) # broad range to
find notch depth
center_idx = np.argmin(np.abs(w_test - omega_n_notch))
for zz in zeta_values:
G_notch = notch_filter(omega_n_notch, zz)
w_test, H = freqresp(G_notch, w_test)
mag_db = 20*np.log10(np.abs(H))
notch_level = mag_db[center_idx]
target_level = -50
left_idx = np.where(mag_db[:center_idx] > target_level)[0]
right_idx = np.where(mag_db[center_idx:] > target_level)[0]
if len(left_idx)>0 and len(right_idx)>0:
left_freq = w_test[left_idx[-1]]
right_freq = w_test[center_idx + right_idx[0]]
bandwidth = right_freq - left_freq
else:
bandwidth = None
BW_values.append(bandwidth if bandwidth is not None else np.nan)

plt.figure()
plt.plot(zeta_values, np.array(BW_values, dtype=np.float64) / (2 * np.pi), 'o-')
plt.xlabel('Zeta')
plt.ylabel('50 dB Attenuation Bandwidth (Hz)')
plt.title('50 dB Attenuation Bandwidth vs Zeta for Notch Filter')
plt.grid(True)
plt.show()

# -------------------------------------
# 2. Frequency Response Verification
# -------------------------------------
# Choose a particular zeta and verify attenuation is ~50 dB
chosen_zeta = 6.41
G_notch_chosen = notch_filter(omega_n_notch, chosen_zeta)

# From 1 Hz to 200 Hz
w_check = np.linspace(2 * np.pi, 2*np.pi*200, 100000)
_, H_chosen = freqresp(G_notch_chosen, w_check)
mag_db_chosen = 20*np.log10(np.abs(H_chosen))

21 / 24
Control CHW No. 6 Erfan Khadem

plt.figure()
# No need for semilogx. A linear plot is easier to read in this context.
plt.plot(w_check/(2*np.pi), mag_db_chosen)
plt.axvline(49, color='r', linestyle=':', label='49 Hz')
plt.axvline(51, color='r', linestyle=':', label='51 Hz')
plt.title(f'Bode Diagram of Notch Filter (Chosen Zeta = {chosen_zeta:.3f})')
plt.xlabel('Frequency (Hz)')
plt.ylabel('Magnitude (dB)')

plt.grid(True)
# Check attenuation around 50 Hz
# Note: The attenuation at 50 Hz is ideally infinity. But here we get
# ~120 dB. This is due to frequency resolution (we are not evaluating the
# TF at exactly 50Hz, we are evaluating at something like 50.00001 Hz)
# As a side effect this makes the plot more readable :shrug:
def annotate_attenuation_at_frequency(freq, text_xy):
around_f_idx = np.argmin(np.abs(w_check - 2 * np.pi * freq))
atten_f = mag_db_chosen[around_f_idx]
plt.annotate(f"Attenuation at {freq} Hz: {atten_f:.1f} dB",
xy=(freq, atten_f), xycoords='data',
xytext=text_xy, textcoords='data',
arrowprops=dict(arrowstyle="->"))

annotate_attenuation_at_frequency(49, (55, -45))


annotate_attenuation_at_frequency(51, (55, -55))
annotate_attenuation_at_frequency(50, (55, -65))
plt.legend()
plt.show()

# -------------------------------------
# 3. Implementation and Testing
# -------------------------------------
# Create a test signal: combination of 10 Hz and 50 Hz sinusoids
fs = 10_000
t = np.linspace(0, 0.5, int(fs*0.5), endpoint=False) # 0.5s duration
signal_10Hz = np.sin(2*np.pi*10*t)
signal_50Hz = np.sin(2*np.pi*50*t)
test_signal = signal_10Hz + signal_50Hz

# Apply notch filter: We have a continuous-time transfer function, but for


simulation:
# Let's discretize or directly perform frequency domain filtering.
# A simple way: use the frequency response and do filtering in freq domain.
# Alternatively, we can design a discrete filter approximation (bilinear
transform).

# For demonstration, do a frequency domain multiplication:


freq_spectrum = np.fft.fftshift(np.fft.fft(test_signal))
freqs = np.fft.fftshift(np.fft.fftfreq(len(t), 1/fs))

22 / 24
Control CHW No. 6 Erfan Khadem

# Convert frequency in Hz to rad/s


omega_vals = 2*np.pi*freqs

# Evaluate notch response at each frequency


_, H_signal = freqresp(G_notch_chosen, omega_vals)
# freqresp only valid for positive freqs; symmetrical for negative
center = len(t) // 2
H_full = np.ones_like(freq_spectrum, dtype=complex)
'''
# Evil code. Brain teaser: Try to figure out how I was trying to optimize
# this and why I failed miserably.
H_full[:len(H_signal)] = np.conjugate(H_signal[::-1])
H_full[-len(H_signal):] = H_signal
'''
H_full = H_signal

filtered_signal_freq = freq_spectrum * H_full


filtered_signal = np.fft.ifft(np.fft.fftshift(filtered_signal_freq)).real

idx = np.where(np.abs(freqs) < 70)

plt.figure()
plt.plot(freqs[idx], np.abs(freq_spectrum[idx]), label='Original Signal')
plt.plot(freqs[idx], np.abs(filtered_signal_freq[idx]), label='Filtered
Signal')
plt.xlabel('Frequency (Hz)')
plt.ylabel('Amplitude')
plt.title('Filter Frequency Response')
plt.grid(True)
plt.legend()
plt.show()

# Time domain comparison


plt.figure()
plt.plot(t, filtered_signal, label='Filtered Signal')
plt.plot(t, signal_10Hz, label='Reference 10Hz Signal', color='red',
linestyle='--')
plt.plot(t, test_signal, label='Original Signal', color='green')
plt.xlabel('Time (s)')
plt.ylabel('Amplitude')
plt.title('Time Domain Signals')
plt.grid(True)
plt.legend()
plt.show()

G_notch_chosen_control = notch_filter(omega_n_notch, chosen_zeta,


for_control=True)
time_domain_response = control.forced_response(G_notch_chosen_control, T=t,
U=test_signal)
td_out = time_domain_response.outputs
plt.figure()
plt.plot(t, td_out, label='Filtered Signal')

23 / 24
Control CHW No. 6 Erfan Khadem

plt.plot(t, signal_10Hz, label='Reference 10Hz Signal', color='red',


linestyle='--')
plt.plot(t, test_signal, label='Original Signal', color='green')
plt.xlabel('Time (s)')
plt.ylabel('Amplitude')
plt.title('Time Domain Signals')
plt.grid(True)
plt.legend()
plt.show()

# Clearly, the 50 Hz component should be attenuated, leaving mostly the 10


Hz component.

# -------------------------------------
# Summary in Comments:
#
# Notch filters are commonly used in control systems to reject specific harmonic
disturbances.
# By placing a pair of complex conjugate zeros at the disturbance frequency and
suitable poles close by,
# we achieve a deep attenuation notch. The damping ratio (zeta) controls how
sharp or broad this notch is:
# - Smaller zeta -> narrower but deeper notch (good for very specific frequency
rejection).
# - Larger zeta -> wider notch but with less attenuation.
#
# Real-world applications include filtering out line-frequency hum in audio
systems, vibration modes
# in mechanical systems, or rejecting a known periodic disturbance in a control
loop.
# See Twin-T filter. Also see bootstrapped Twin-T filter, which utilizes feedback
to
# control the quality factor of the system. In real world scenarios around 40
to 60 dBs worth
# of attenuation can be extracted from common off the shelf non-precision
components on a breadboard.

24 / 24

You might also like