École Polytechnique Fédérale de Lausanne
EE-426: Radio Frequency Circuits Design Techniques
Lab 1: BPSK Modulation and RLC Filterings
¶Filippo Quadri
filippo.quadri@epfl.ch
Welcome to the first lab of the EE-426 course!¶
In this session, we'll explore the architecture of a basic digital communication system, focusing on two key components:
- A BPSK (Binary Phase Shift Keying) modulation/demodulation system.
- A simple RLC analog filter, modeled first in Python, then simulated using Cadence.
The goal of this lab is to build intuition about how digital information can be transmitted over analog channels and how basic filtering plays a role in signal conditioning.
Lab Outline¶
Part 1: BPSK Modulation System¶
- Understand BPSK and its role in digital communication.
- Implement a BPSK transmitter in Python.
- Visualize the modulated signal and bitstream.
- Implement a BPSK receiver in Python.
Part 2: RLC Low-Pass Filter¶
- Pen&Paper analysis of the analog filter
- Use Python to size components of a basic RLC filter.
- Analyze its frequency response.
- Simulate the same circuit in Cadence to verify its behavior (next session)
Let's get started!
Part 1: BPSK Modulation System¶
Before we dive into the implementation, let’s take a moment to understand what we’re going to build.
Digital Communication Basics¶
Even though we often prefer to treat information digitally (as bits), in the real world, communication usually happens over analog, continuous-time channels — such as air, cables, or optical fibers. To send digital data through these analog media, we need to convert it into a form that can be transmitted effectively.
This typically involves four major steps:
Baseband Conversion:
First, we map the digital information (a sequence of 0s and 1s) into a baseband analog signal — a signal that varies smoothly in time and carries our digital data.Modulation:
We then modulate a carrier wave — typically a high-frequency sinusoidal signal — to carry the baseband signal. This process translates our signal to higher frequencies, suitable for efficient transmission.Transmission:
Finally, the modulated signal is sent through a physical medium (like air using an antenna, or through a wired channel).Reception, Demodulation: The ultimate goal of a communication system is to transmit data accurately from one point to another. To achieve this, a receiver must be equipped with proper demodulation logic to recover the original signal. Additionally, the receiver may apply error correction techniques to improve the reliability and integrity of the received data.
Why Do We Use a Carrier?¶
The reasons for using a high-frequency carrier signal go beyond this lab's scope, but here's a quick overview of the main motivations:
Efficient Radiation:
Antennas are more efficient at radiating energy at higher frequencies. For example, a 1 kHz baseband signal can't be transmitted effectively over the air, but a 1 GHz carrier wave can.Multiplexing:
Modulating signals onto different carrier frequencies allows multiple users or systems to share the same medium without interfering with each other. This is how FM radio stations or cellular networks work (each station uses a different frequency band).Filtering and Noise Reduction:
Moving the signal to a higher frequency band makes it easier to filter and isolate from low-frequency noise or interference (e.g. flicker noise, DC noise, ...)Bandwidth Utilization:
High-frequency bands often provide wider bandwidths, allowing for higher data rates.
What is BPSK?¶
BPSK (Binary Phase Shift Keying) is one of the simplest forms of digital modulation. In BPSK, each bit of a binary message (either 0 or 1) is represented by a phase shift of the carrier signal, defined as follow:
- Bit
1→ carrier phase = 0° - Bit
0→ carrier phase = 180°
This means:
- A
1is transmitted as: $+A\cos(2\pi f_ct)$ - A
0is transmitted as: $-A\cos(2\pi f_ct)$
Where:
- $A$ is the amplitude of the carrier signal.
- $f_c$ is the carrier frequency.
- $t$ is time.
Why Use BPSK?¶
- Simplicity: Easy to implement in both hardware and software.
- Efficiency: BPSK is robust against noise compared to some other modulation schemes.
- Binary Logic: Perfectly matches digital (0/1) systems.
Let's start building the system!
Note 1: a real system is much more complex compared to the one we're building (e.g. pilot signal, ...), but still it will give a general overview on the architecture of the system.
Note 2. Although BPSK (Binary Phase Shift Keying) belongs to the PSK family — which is fundamentally based on phase modulation — it can also be interpreted as a special case of amplitude modulation. This is because BPSK effectively transmits data by inverting the sign of the carrier wave (i.e., toggling between +A and −A), which can be seen as an amplitude change between two discrete levels, even though the amplitude magnitude remains constant.
Code¶
After covering some basic theory on digital communication, we're ready to start building our system. All the necessary setup — including library installation, imports, utility functions for converting strings to bitstreams (and back), as well as other tools (filters, visualizations, ...) — has been provided.
%pip install numpy matplotlib scikit-learn seaborn scipy schemdraw --quiet
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import schemdraw
import schemdraw.elements as elm
from typing import Tuple, List
Note: you may need to restart the kernel to use updated packages.
String/Bitstream Convertion Functions
def string2bitstream(s: str) -> np.ndarray:
"""
Converts a string into a bitstream represented as a NumPy array of 0s and 1s.
Each character in the input string is encoded using its 8-bit ASCII binary representation.
The result is a flat array containing the entire bitstream.
Parameters
----------
s : str
Input string to be converted.
Returns
-------
np.ndarray
1D NumPy array of integers (0s and 1s), representing the bitstream of the input string.
"""
return np.array([int(bit) for char in s for bit in f"{ord(char):08b}"])
def bitstream2string(b: np.ndarray) -> str:
"""
Converts a bitstream represented as a NumPy array of 0s and 1s back into a string.
The input bitstream is expected to be a flat array where each group of 8 bits corresponds
to the ASCII binary representation of a character.
Parameters
----------
b : np.ndarray
1D NumPy array of integers (0s and 1s), representing the bitstream.
Returns
-------
str
The decoded string from the input bitstream.
"""
chars = [chr(int("".join(map(str, b[i:i+8])), 2)) for i in range(0, len(b), 8)]
return "".join(chars)
Visualization Functions
def plot_waveform(
signal: np.ndarray,
bit_rate: float,
sampling_freq: float,
title: str = "Baseband BPSK Modulated Signal",
figsize=(15, 4),
):
"""
Plots a baseband BPSK signal using seaborn/matplotlib.
Parameters
----------
signal : np.ndarray
The modulated baseband signal (1D array).
bit_rate : float
Bit rate in bits per second.
sampling_freq : float
Sampling frequency in Hz.
title : str
Plot title.
figsize : tuple
Size of the figure.
"""
sns.set_theme(style="whitegrid")
bit_duration = 1 / bit_rate
total_time = len(signal) / sampling_freq
t = np.linspace(0, total_time, len(signal))
fig, ax = plt.subplots(figsize=figsize)
ax.plot(t, signal, color=sns.color_palette("muted")[0], lw=2, label="BPSK signal")
ax.set_title(title, fontsize=14)
ax.set_xlabel("Time [s]")
ax.set_ylabel("Amplitude")
ax.set_xlim(0, total_time)
sns.despine()
plt.tight_layout()
plt.show()
def plot_fft(
signals: list,
sampling_freq: float,
labels: list = None,
normalize: bool = True,
xlim: tuple = None,
title: str = "FFT Spectrum",
figsize: tuple = (16, 8)
):
"""
Plot the FFT magnitude spectrum of one or more signals.
Parameters
----------
signals : list of np.ndarray
List of 1D signals (same length preferred).
sampling_freq : float
Sampling frequency in Hz.
labels : list of str, optional
Labels for each signal in the legend.
window : str, optional
Window function name (e.g., 'hamming', 'hann').
normalize : bool
If True, normalize magnitude to [0, 1].
xlim : tuple, optional
Limits for x-axis (e.g., (0, 5000)).
title : str
Title of the plot.
figsize : tuple
Size of the figure.
"""
sns.set_theme(style="whitegrid")
plt.figure(figsize=figsize)
for i, sig in enumerate(signals):
N = len(sig)
fft_vals = np.fft.fft(sig)
fft_freqs = np.fft.fftfreq(N, d=1/sampling_freq)
# Keep only positive frequencies
mask = fft_freqs >= 0
fft_freqs = fft_freqs[mask]
fft_mags = np.abs(fft_vals[mask])
if normalize:
fft_mags /= np.max(fft_mags)
label = labels[i] if labels and i < len(labels) else f"Signal {i+1}"
plt.plot(fft_freqs, fft_mags, label=label)
plt.title(title, fontsize=14)
plt.xlabel("Frequency [Hz]")
plt.ylabel("Normalized Magnitude" if normalize else "Magnitude")
if xlim:
plt.xlim(xlim)
plt.legend()
plt.grid(True, linestyle='--', alpha=0.5)
plt.tight_layout()
plt.show()
I also provide you with the general structure of the BPSK modulator. In the following steps, you’ll complete each function to build a fully working modulation system.
For your reference, all key details are explained throughout the lab. However, each function also includes a docstring — a detailed comment at the beginning of the function — which gives you a quick intuition about how it works. When you use the function in your code, the docstring will automatically appear (e.g., when you hover or use autocomplete), providing you with everything you need to understand and implement it correctly.
from scipy.signal import convolve
class BPSKModulator:
def __init__(self, bit_rate: float, carrier_freq: float, sample_rate: float):
"""
Initializes the BPSKModulator with specified parameters.
Parameters
----------
bit_rate : float
Bit rate of the input signal in bits per second.
carrier_freq : float
Frequency of the carrier signal in Hz.
sample_rate : float
Sampling rate for the modulation process in Hz.
"""
self.bit_rate = bit_rate
self.carrier_freq = carrier_freq
self.sample_rate = sample_rate
def _generate_baseband_waveform(self, bitstream: np.ndarray) -> np.ndarray:
"""
Generates the baseband waveform for BPSK modulation.
Each bit in the input bitstream is represented as a rectangular pulse:
- A '1' bit is represented by a pulse of +1.
- A '0' bit is represented by a pulse of -1.
Parameters
----------
bitstream : np.ndarray
1D NumPy array of integers (0s and 1s), representing the bitstream.
Returns
-------
np.ndarray
Array containing the generated baseband waveform samples.
"""
samples_per_bit = int(self.sample_rate / self.bit_rate) # Hidden from students
baseband_waveform = np.repeat(2 * bitstream - 1, samples_per_bit) # Hidden from students
return baseband_waveform
def _generate_carrier(self, nb_samples: int) -> np.ndarray:
"""
Generates a carrier wave for BPSK modulation.
The carrier wave is a cosine wave with a frequency defined by carrier frequency (class variable).
Parameters
----------
nb_samples : int
Number of samples to generate for the carrier wave.
Returns
-------
np.ndarray
Array containing the generated carrier wave samples.
"""
t = np.arange(nb_samples) / self.sample_rate # Hidden from students
carrier = np.cos(2 * np.pi * self.carrier_freq * t) # Hidden from students
return carrier
def _rrc_filter(
self,
signal: np.ndarray,
alpha: float = 0.35,
span: int = 6,
sps: int = None
) -> np.ndarray:
"""
Applies a Root Raised Cosine (RRC) filter to the input signal.
Parameters
----------
signal : np.ndarray
The input signal to filter.
alpha : float, optional
Roll-off factor (0 < alpha ≤ 1). Default is 0.35.
span : int, optional
The number of symbols the filter spans. Default is 6.
sps : int, optional
Samples per symbol (default inferred from sample_rate and bit_rate).
Returns
-------
np.ndarray
The filtered signal.
"""
if sps is None:
sps = int(self.sample_rate / self.bit_rate)
# Time vector for the filter taps
t = np.arange(-span / 2, span / 2 + 1/sps, 1/sps)
# Avoid division by zero
t = np.where(t == 0, 1e-8, t)
pi_t = np.pi * t
four_alpha_t = 4 * alpha * t
numerator = np.sin(pi_t * (1 - alpha)) + 4 * alpha * t * np.cos(pi_t * (1 + alpha))
denominator = pi_t * (1 - (four_alpha_t) ** 2)
h = numerator / denominator
# Handle NaNs or division artifacts
h = np.nan_to_num(h)
# Normalize filter energy
h /= np.sqrt(np.sum(h ** 2))
# Filter the signal via convolution
filtered_signal = convolve(signal, h, mode='same')
return filtered_signal
def modulate(self, bitstream: np.ndarray) -> np.ndarray:
"""
Modulates the input bitstream using BPSK modulation.
The modulation process involves generating a baseband waveform from the bitstream,
creating a carrier wave, and then multiplying the baseband waveform with the carrier wave.
A low-pass filter is applied to the modulated signal to smooth it.
Parameters
----------
bitstream : np.ndarray
1D NumPy array of integers (0s and 1s), representing the bitstream to be modulated.
Returns
-------
np.ndarray
1D NumPy array representing the modulated BPSK signal.
"""
baseband_waveform = self._generate_baseband_waveform(bitstream) # Hidden from students
filtered_baseband = self._rrc_filter(baseband_waveform) # Hidden from students
carrier = self._generate_carrier(len(filtered_baseband)) # Hidden from students
modulated_signal = filtered_baseband * carrier # Hidden from students
return modulated_signal
def demodulate(self, received_signal: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
"""
Demodulates the received BPSK signal to recover the original bitstream.
The demodulation process involves multiplying the received signal with the carrier wave,
applying a low-pass filter to extract the baseband signal, and then sampling the baseband
signal to determine the transmitted bits.
Parameters
----------
received_signal : np.ndarray
1D NumPy array representing the received BPSK signal.
Returns
-------
Tuple[np.ndarray, np.ndarray]
A tuple containing:
- 1D NumPy array representing the recovered bitstream (0s and 1s).
- 1D NumPy array representing the mixed signal before filtering.
"""
carrier = self._generate_carrier(len(received_signal)) # Hidden from students
mixed_signal = received_signal * carrier # Hidden from students
filtered_signal = self._rrc_filter(mixed_signal) # Hidden from students
samples_per_bit = int(self.sample_rate / self.bit_rate) # Hidden from students
nb_samples = len(received_signal) # Hidden from students
recovered_bitstream = np.zeros(nb_samples // samples_per_bit) # Hidden from students
# Hidden from students
for i in range(nb_samples // samples_per_bit):
bit_chunk = filtered_signal[i * samples_per_bit : (i + 1) * samples_per_bit]
recovered_bitstream[i] = 1 if np.sum(bit_chunk) > 0 else 0
return recovered_bitstream.astype(int), mixed_signal
Step 1: Generate the Baseband Waveform¶
The first step is to convert the bitstream into a baseband signal. A baseband signal is one whose frequency content is centered around DC (zero frequency). It typically contains low-frequency components and is symmetric around zero if we consider negative frequencies — but that's a detail more relevant to signal processing experts.
Our objective here is to map the binary sequence of 0s and 1s into a sequence of -1s and +1s.
Your Answer: $$ bb[t] = 2 \cdot bs[t] - 1 $$
Use this function to complete the _generate_baseband_waveform method. If your implementation is correct, the next cell should execute without errors.
In the BPSKModulator class, several variables are initialized in the
__init__ method (the class constructor). You can access these from any method within the class using self.variable_name.
Python is beginner-friendly but relatively slow since it's interpreted. To improve performance, it's recommended to use optimized libraries like NumPy, which are built on C. Make use of NumPy functions whenever possible for this and upcoming tasks.
bpsk_mod = BPSKModulator(bit_rate=1000, carrier_freq=10000, sample_rate=100000)
# Generate the bitstream
bitstream = string2bitstream("This Lab is amazing!")
# Create the BPSK Baseband Waveform
baseband_waveform = bpsk_mod._generate_baseband_waveform(bitstream)
# Visualize the Baseband Waveform
plot_waveform(
signal=baseband_waveform,
bit_rate=bpsk_mod.bit_rate,
sampling_freq=bpsk_mod.sample_rate,
title="Baseband BPSK Signal",
)
Let's also have a look at the FFT of the signal
plot_fft(
signals=[baseband_waveform],
sampling_freq=bpsk_mod.sample_rate,
labels=["Baseband BPSK"],
xlim=(0, 15000),
title="FFT of Baseband BPSK Signal",
normalize=False
)
As you can see, the frequency response of this signal is not confined to a limited bandwidth.
Your Answer: A pulse waveform (such as a rectangular pulse) contains sharp transitions in time, which translate to high-frequency components in the frequency domain. As a result, its Fourier Transform extends infinitely, meaning the signal has no strictly limited bandwidth.
To reduce the bandwidth of the signal, we can apply a low-pass filter. As shown, the majority of the signal’s energy is concentrated within a bandwidth of approximately 1kHz. This is intuitive: for baseband digital modulation schemes, the effective (or "occupied") bandwidth of the signal can be approximated as
$$ BW \approx \frac{1}{T_B} = BR $$
where $T_B$ is the bit duration and $BR$ is the bit rate.
A widely used filter in digital communications for bandwidth shaping is the Root-Raised-Cosine (RRC) filter, which is already implemented for you.
# Filter the baseband signal to limit its bandwidth
filtered_baseband = bpsk_mod._rrc_filter(baseband_waveform)
# Visualize the FFT comparison between original and filtered baseband signals
plot_fft(
signals=[baseband_waveform, filtered_baseband],
sampling_freq=bpsk_mod.sample_rate,
labels=["Original Baseband BPSK", "Filtered Baseband BPSK"],
xlim=(0, 15000),
title="FFT of Filtered and Original Baseband BPSK Signal"
)
# Visualize the Filtered Baseband Waveform
plot_waveform(
signal=filtered_baseband,
bit_rate=bpsk_mod.bit_rate,
sampling_freq=bpsk_mod.sample_rate,
title="Filtered Baseband BPSK Signal",
)
Step 2: Generate the Carrier Wave¶
As discussed in the theoretical section, BPSK modulation is achieved by modulating the phase (or equivalently, the sign) of a carrier wave. The carrier signal is defined by the following equation:
$$ c(t) = \cos(2\pi f_c t) $$
where $f_c$ is the carrier frequency.
Using this expression, you can now complete the _generate_carrier method.
Step 3: Generate the modulated signal¶
Now that you created all the components you need to modulate the signal, you can put everything together inside the modulate method. If you succed, you will see a part of the modulated signal and it's fft in the cell below.
modulated_signal = bpsk_mod.modulate(bitstream)
plot_waveform(
signal=modulated_signal[:2000],
bit_rate=bpsk_mod.bit_rate,
sampling_freq=bpsk_mod.sample_rate,
title="BPSK Modulated Signal",
)
plot_fft(
signals=[baseband_waveform, modulated_signal],
sampling_freq=bpsk_mod.sample_rate,
labels=["Original Baseband BPSK", "Modulated BPSK"],
xlim=(0, 15000),
title="FFT of Modulated and Original Baseband BPSK Signal",
)
Step 4: Restore the Signal¶
Let's do some math... At the receiver, what we have is the modulated signal (without noise for simplicity). $$ bpsk\_mod(t) = m(t) \cdot \cos (2\pi f_c t) $$
where $m(t)$ is the original message (in it's Baseband form). It can be shown that $m(t)$ can be recovered simply by doing
$$ \hat{m}(t) = LP\left[ 2 \cdot bpsk\_mod(t) \cdot \cos (2\pi f_c t)\right] $$
where $LP$ is the application of some lowpass filter. From now on $ bpsk\_mod(t) \cdot \cos (2\pi f_c t) $ will be called the mixed signal $mix(t)$, as it is the output of the analog mixer in the analog implementation.
Your Answer: $$ \begin{align} mix(t) &= bpsk\_mod(t) \cdot \cos (2\pi f_c t) \\ mix(t) \cdot \cos (2\pi f_c t) &= bpsk\_mod(t) \cdot \cos^2 (2\pi f_c t) \\ &= \frac{1}{2}bpsk\_mod(t) \cdot \left( \cos (4\pi f_c t) + 1 \right) \\ &\Rightarrow \times 2 \text{ and } LP \\ \hat{m}(t) &= LP\left[ 2 \cdot bpsk\_mod(t) \cdot \cos (2\pi f_c t)\right] \end{align} $$
Implement the demodulate method. If you implement everything correctly you should be able to recover the original message :)
restored_bitstream, mixed_signal = bpsk_mod.demodulate(modulated_signal)
restored_message = bitstream2string(restored_bitstream)
print("Restored Message:", restored_message)
plot_fft(
signals=[mixed_signal],
sampling_freq=bpsk_mod.sample_rate,
labels=["Mixed Signal"],
xlim=(0, 25000),
title="FFT of Mixed Signal Before Filtering",
)
Restored Message: This Lab is amazing!
Part 2: RLC Filtering¶
In this section, we will analyze a second-order low-pass filter configuration based on an RLC resonator. The circuit we will study is shown in the figure in the next cell.
with schemdraw.Drawing() as d:
d.config(unit=3)
d.push()
elm.Inductor2().left().flip().label('L')
elm.SourceSin().down()
elm.Ground().right()
d.pop()
C = elm.Capacitor2().down().label('C')
elm.Line().left()
d.pop()
elm.Line().right(1.5).at(C.start)
R = elm.Resistor().down().label('R')
elm.Line().left(1.5)
d.pop()
elm.Line().right(1).at(R.start)
elm.Line().right(1).at(R.end)
elm.Gap().toy(R.start).label(['-', '$V_{out}$', '+'])
Theoretical Analysis¶
When we talk about filters, the first concept that should come to mind is the Transfer Function. This function characterizes how the circuit responds to different frequencies and is defined as
$$ H(j\omega) = \frac{V_{out}(j\omega)}{V_{in}(j\omega)} $$
Hint: to simplify the next steps, write the transfer function in the following form $$ H(j\omega) = \frac{1}{1+j\omega\cdot A + (j\omega)^2 \cdot B} $$
Your Answer: $$ \begin{align} Y_{RC} &= \frac{1}{R} + j\omega C \\ \Rightarrow Z_{RC} &= \frac{R}{1 + j\omega RC} \\ \\ Z_{tot} &= j\omega L + \frac{R}{1 + j\omega RC} \\ &= \frac{j\omega L + (j\omega)^2LRC + R}{1 + j\omega RC} \\ \\ \frac{V_{out}}{V_{in}} &= \frac{R}{R + j\omega L + (j\omega)^2LRC}\\ &= \frac{1}{1+j\omega \frac{L}{R} + (j\omega)^2LC} \end{align} $$ $$ \boxed{\frac{V_{out}}{V_{in}} = \frac{1}{1+j\omega \frac{L}{R} + (j\omega)^2LC}} $$
Great! Now that we have the transfer function, we can express it in an alternative form:
$$ \frac{V_{out}}{V_{in}} = \frac{1}{1 + j 2 \zeta \frac{\omega}{\omega_0} + \left(j \frac{\omega}{\omega_0}\right)^2} $$
where $\omega_0$ represents the resonance frequency of the RLC circuit, and $\zeta$ denotes the damping ratio.
Your Answer: $$ \begin{align} H(j\omega) &= \frac{1}{1+j\frac{\omega}{\omega_0}2 \zeta + (j\frac{\omega}{\omega_0})^2}\\ \omega_0 &= \sqrt{\frac{1}{LC}}\\ \zeta &= \frac{1}{2R}\sqrt{\frac{L}{C}} \end{align} $$
Let's have a closer look to this transfer function. When $\omega_0 << \omega$ (high frequencies) the transfer function can be approximated by $$ H(j\omega) \approx -\frac{1}{(\frac{\omega}{\omega_0})^2} $$
That is, basically, a slope of $-40dB/dec$
When $\omega << \omega_0$ (low frequencies), the transfer function can be approximated by $$ H(j\omega) \approx 1 $$
so a unity gain (slope 0)
The interesting part is around $\omega_0$ where the system may or may not have a peak in the transfer function (due to the resonance) depending on the value of $\zeta$. Let's dive into this analysis.
To find the peak, we can check for the maximum of the magnitude of the transfer function.
Hint 1: there will be two solutions, one will have a peak and the other one not
Your Answer: Let's define the magnitude of the transfer function as $$ M(\omega) = \Big| G(j\omega) \Big| = \frac{1}{\sqrt{\left( 1 - \left( \frac{\omega}{\omega_0}\right)^2 \right)^2 + 4\zeta^2 \left( \frac{\omega}{\omega_0}\right)^2}} $$ The peak occurs when $\frac{\partial}{\partial \omega}M(\omega)=0$, which is when $\frac{\partial}{\partial \omega}\left[\left( 1 - \left( \frac{\omega}{\omega_0}\right)^2 \right)^2 + 4\zeta^2 \left( \frac{\omega}{\omega_0}\right)^2\right] = 0$
After the derivative calculation and simplification, this leads to $$ 4\left( \frac{\omega}{\omega_0}\right)\left( \left( \frac{\omega}{\omega_0}\right)^2 + 2\zeta^2-1 \right) = 0 $$
Two cases are now possible
If $2\zeta^2-1 > 0$, i.e. $\zeta \geq \frac{1}{\sqrt{2}}$ :
The only solution is $\omega = 0$, so no resonant peakIf $2\zeta^2-1 < 0$, i.e. $0\leq \zeta \leq \frac{1}{\sqrt{2}}$ :
There is a peak at $\left(\frac{\omega}{\omega_0}\right) = \sqrt{1- 2\zeta^2}$
We define the peak frequency $\omega_P$ as $$ \boxed{\omega_P = \omega_0\sqrt{1- 2\zeta^2}} $$
Your Answer: $$ M(\omega_P) = \frac{1}{2\zeta\sqrt{1-\zeta^2}} $$
Alright, everything should be starting to make sense now. For the final part, I'll provide you with most of the code, and your task is simply to complete a function that calculates the values of $R$, $L$, and $C$ based on the damping factor $\zeta$ and cutoff frequency.
Once done, you can use the plot_bode function to visualize the frequency response of the filter.
Please plot the frequency response for the following damping factors:
- $\zeta = 0.01$
- $\zeta = 0.1$
- $\zeta = \sqrt{2}$
- $\zeta = 1$
The cutoff frequency is set to 100 kHz, and the inductance $L$ is fixed at $10 \, \mu H$.
Remember, the cutoff frequency corresponds to the resonance frequency $\omega_0$ of the RLC oscillator.
def size_lp_filter(zeta: float, f_c: float, L: float = 10e-6) -> Tuple[float, float, float]:
"""
Computes the values of R and C for a second-order low-pass RLC filter given the damping factor (zeta),
cutoff frequency (f_c), and inductance (L).
Parameters
----------
zeta : float
Damping factor of the filter.
f_c : float
Cutoff frequency in Hz.
L : float
Inductance in Henry.
Returns
-------
Tuple[float, float, float]
A tuple containing:
- R : Resistance in Ohms.
- C : Capacitance in Farads.
- L : Inductance in Henry (same as input).
"""
omega_c = 2 * np.pi * f_c # Hidden from students
C = 1 / (L * (omega_c ** 2)) # Hidden from students
R = 1 / (2 * zeta) * np.sqrt(L / C) # Hidden from students
print(f"Zeta = {zeta:.2f} => R: {R:.2f} Ohms, C: {C*1e9:.2f} nF, L: {L*1e6:.2f} uH")
return R, C, L
def plot_bode(
RLC: List[Tuple[float, float, float]],
dB=True,
deg=True,
freq_range=None,
title='Bode Diagram',
figsize=(10, 6)
) -> None:
"""
Plots the Bode diagram (magnitude and phase) for one or more RLC low-pass filters.
Parameters
----------
RLC : List[Tuple[float, float, float]]
List of tuples, each containing (R, C, L) values for the filters.
dB : bool
If True, plot magnitude in dB. Default is True.
deg : bool
If True, plot phase in degrees. Default is True.
freq_range : tuple, optional
Frequency range for the x-axis (min_freq, max_freq). Default is None (auto).
title : str
Title of the plot.
figsize : tuple
Size of the figure.
"""
sns.set_theme(style="whitegrid")
plt.figure(figsize=figsize)
for rlc in RLC:
r, c, l = rlc
zeta = 1/(2*r) * np.sqrt(l/c) # Hidden from students
omega_0 = 1/np.sqrt(l*c) # Hidden from students
num = [1]
den = [1/(omega_0**2), 2*zeta/omega_0, 1]
# Frequency response
w = np.logspace(1, 8, 1000) if freq_range is None else np.logspace(np.log10(freq_range[0]), np.log10(freq_range[1]), 1000)
s = 1j * w
H = np.polyval(num, s) / np.polyval(den, s)
magnitude = 20 * np.log10(np.abs(H)) if dB else np.abs(H)
phase = np.angle(H, deg=deg)
label = f'$\\zeta$={zeta:.2f}'
plt.subplot(2, 1, 1)
plt.semilogx(w/(2*np.pi), magnitude, label=label)
plt.title(title)
plt.ylabel('Magnitude (dB)' if dB else 'Magnitude')
plt.grid(True, which='both', linestyle='--', alpha=0.5)
plt.subplot(2, 1, 2)
plt.semilogx(w/(2*np.pi), phase, label=label)
plt.xlabel('Frequency (Hz)')
plt.ylabel('Phase (degrees)' if deg else 'Phase (radians)')
plt.grid(True, which='both', linestyle='--', alpha=0.5)
plt.subplot(2, 1, 1)
plt.legend()
plt.tight_layout()
plt.show()
zeta_values = [0.01, 0.1, np.sqrt(2), 1]
RLC = [size_lp_filter(zeta=zeta, f_c=100e3) for zeta in zeta_values]
plot_bode(
RLC=RLC,
freq_range=(1e3, 10e8),
title='Bode Diagram of RLC Low-Pass Filters'
)
Zeta = 0.01 => R: 314.16 Ohms, C: 253.30 nF, L: 10.00 uH Zeta = 0.10 => R: 31.42 Ohms, C: 253.30 nF, L: 10.00 uH Zeta = 1.41 => R: 2.22 Ohms, C: 253.30 nF, L: 10.00 uH Zeta = 1.00 => R: 3.14 Ohms, C: 253.30 nF, L: 10.00 uH