EE3150-LAB3/LAB3.tex
2025-10-21 23:57:06 -04:00

481 lines
15 KiB
TeX

\documentclass{article}
\usepackage[margin=1in]{geometry}
\usepackage{graphicx}
\usepackage{minted}
\usepackage{amsmath}
\usepackage{hyperref}
\newcommand{\RE}[1]{\mathrm{Re} \left \{ #1 \right \}}
\newcommand{\IM}[1]{\mathrm{Im} \left \{ #1 \right \}}
\usepackage{longtable,booktabs,array}
\usepackage{siunitx}
\usepackage{commath}
\newcommand\numberthis{\addtocounter{equation}{1}\tag{\theequation}}
\usepackage{amsfonts}
\usepackage{cancel}
\usepackage[T1]{fontenc}
\usepackage{framed}
\usepackage{caption}
\usepackage{gensymb}
\title{Lab 3, EE3150}
\author{Martin Kennedy and DJ}
\begin{document}
\maketitle
\section{Introduction}
In this lab, we investigate the response of a circuit (seen in figure
\ref{img:circuit_diagram}) to different sinusoidal signals. We apply
both symbolic analysis and numeric analysis (simulation) to affirm
that this causal LTI system will always generate a sinusoidal response
of the same frequency as that provided to it.
\begin{figure}[h]
\caption{A diagram of our two-stage RC circuit, with a fixed-gain amplifier between the two stages to act as a buffer}
\label{img:circuit_diagram}
\centering
\includegraphics[width=0.8\textwidth]{circuit_diagram}
\end{figure}
\section{Discussion}
A number of different sinusoidal input signals are considered.
From the Lab3 description, we have:
\subsubsection{Lab3 section 2.2.a.}
$x(t) = A \sin(\omega t)$, for $A = 1 V_{pp}, f \in \left \{1, 5, 10, 100 \right \} \SI{}{\hertz}$.
\subsubsection{Lab3 section 2.2.b.}
$x_1(t) + x_2(t)$, with $x_1(t) = A_1 \sin(\omega_1 t)$,
$x_2(t) = A_2 \sin(\omega_2 t)$, and $(f_1, f_2, A_1, A_2)$ with such
values as
$(\SI{50}{\hertz}, \SI{100}{\hertz}, \SI{1}{V}, \SI{0.5}{V})$ and
$(\SI{50}{\hertz}, \SI{100}{\hertz}, \SI{2}{V}, \SI{4}{V})$.
\subsubsection{Lab3 section 2.2.c.}
As with 2.2.a, but with $x(t) = A \sin(\omega t - 0.025)$, for
$A = 1 V_{pp}, f = \SI{10}{\hertz}$ (i.e., a 0.025 second delay).
\section{Measurement data and/or Results}
\subsubsection{Lab3 section 2.2.a.}
Figures \ref{img:1hz_show}, \ref{img:5hz_show}, \ref{img:10hz_show}
and \ref{img:100hz_show} depict a theoretical $1V_{pp}$ input at
$f \in \left \{1, 5, 10, 100 \right \} \SI{}{\hertz}$, with output
calculated first by convolution with the impulse response, then by
solution of the representative ODE, and finally our experimentally
measured input and output.
This yields the following RMSE values in Table \ref{table:rmse1}.
\begin{longtable}[]{@{}ll@{}}
\toprule
\caption {RMSE of our measured response versus our convolution prediction}
\label{table:rmse1}
\endhead
\bottomrule
\endlastfoot
Signal & RMSE, when compared to $(h*x)$ \\
f = \SI{1}{\hertz} & 3.021 \\
f = \SI{5}{\hertz} & 2.957 \\
f = \SI{10}{\hertz} & 2.691 \\
f = \SI{100}{\hertz} & 0.295 \\
\end{longtable}
\begin{figure}[h]
\caption{A $1V_{pp}$ @ $\SI{1}{\hertz}$ input to our circuit, in theory and in practice, and respective outputs}
\label{img:1hz_show}
\centering
\includegraphics[width=0.8\textwidth]{img/1hz_show}
\end{figure}
\begin{figure}[h]
\caption{A $1V_{pp}$ @ $\SI{5}{\hertz}$ input to our circuit, in theory and in practice, and respective outputs}
\label{img:5hz_show}
\centering
\includegraphics[width=0.8\textwidth]{img/5hz_show}
\end{figure}
\begin{figure}[h]
\caption{A $1V_{pp}$ @ $\SI{10}{\hertz}$ input to our circuit, in theory and in practice, and respective outputs}
\label{img:10hz_show}
\centering
\includegraphics[width=0.8\textwidth]{img/10hz_show}
\end{figure}
\begin{figure}[h]
\caption{A $1V_{pp}$ @ $\SI{100}{\hertz}$ input to our circuit, in theory and in practice, and respective outputs}
\label{img:100hz_show}
\centering
\includegraphics[width=0.8\textwidth]{img/100hz_show}
\end{figure}
This section was generated using the function
\texttt{run\_2\_2\_a\_conv\_pred()} as seen in the Source code section
below, as of commit cad74a446ac2ebbde58e587b93dc2d2b53da3404.
\subsubsection{Lab3 section 2.2.b.}
This next section was more difficult. Because of the very coarse
measurement, synchronizing automatically eluded the techniques we had
on hand in Python, so calculating sums was very difficult. Had we
known this would be so difficult in software, a hardware approach
would have been taken to trigger at zero-crossings and ensure all
samples were recorded in a synchronous manner in the first place.
\begin{figure}[h]
\caption{A combination of sinusoids}
\label{img:2_2_b_combined}
\centering
\includegraphics[width=0.8\textwidth]{img/2_2_b_combined}
\end{figure}
The imagery in this section was created using the function
\texttt{run\_2\_2\_b\_show()} from the Source code section below.
\subsubsection{Lab3 section 2.2.c.}
Incomplete.
\section{Discussion of Measurements, experiments and/or simulations}
Regarding section 2.2.a., the RMSE values were enormous, likely
because of a failure to assemble the circuit correctly. In figure
\ref{img:circuit_assembled}, we can clearly see a pair of
$\SI{4.7}{\kohm}$ resistors R4 and R5; one should instead have been a
$\SI{1}{\kohm}$ resistor. This would have impacted our gain value from
the buffer, which we calculate as $1 + \frac{R5}{R4} \approx 5$.
\begin{figure}[h]
\caption{Our circuit, assembled.}
\label{img:circuit_assembled}
\centering
\includegraphics[width=0.8\textwidth]{img/circuit_assembled}
\end{figure}
\subsection{Source code}
The source code for this lab, and the source code below, can be found
at \url{https://git.laboratoryb.org/hurricos/EE3150-LAB3}.
\begin{minted}[xleftmargin=\parindent,linenos]{python}
# ==== Lab #3: Convolution & Sinusoids ====
import numpy as np
import matplotlib.pyplot as plt
from scipy.integrate import solve_ivp
import csv
import io
# Components (same as Lab #2)
R1, C1 = 10e3, 1e-6
R2, C2 = 4.7e3, 1e-6
tau1, tau2 = R1*C1, R2*C2
R4, R5 = 1e3, 4.7e3
G = 1 + R5/R4
# Time grid
dt = 0.0001
t = np.arange(0, 5 + dt/2, dt) # Includes the final element.
N = t.size
# Impulse responses (causal)
h1 = (1/tau1) * np.exp(-t/tau1) # u(t) implicit (0 for t<0)
h2 = (1/tau2) * np.exp(-t/tau2)
# Overall impulse response for cascaded stages (discrete conv scaled by dt)
h = G * dt * np.convolve(h1, h2, mode='full')
# Helper: numeric convolution with step dt (Riemann sums)
def conv_num(a, b, dt):
"""Full-length discrete convolution approximating integrals via Riemann sums with step dt."""
return np.convolve(a, b, mode='full') * dt
def plot_single_sinusoid(f, dur, dt, do_plot):
# ======================
# 1) Single sinusoids
# ======================
A = 1.0
xmax = dur
t = np.arange(0, dur + dt/2, dt) # Includes the final element.
N = t.size
plt.figure(num='Window')
plt.clf()
w = 2*np.pi*f
x = A * np.sin(w*t) * (t >= 0)
y_full = conv_num(x, h, dt)
y = y_full[:N] # keep same length as x
# ODE: tau1*tau2*y'' + (tau1+tau2)*y' + y = G*x(t)
def rhs(ti, z):
x_t = A*np.sin(w*ti) * (ti >= 0.0)
return [
z[1],
(G*x_t - (tau1 + tau2)*z[1] - z[0]) / (tau1*tau2)
]
z0 = [0.0, 0.0] # zero initial conditions
sol = solve_ivp(rhs, (t[0], t[-1]), z0, t_eval=t, rtol=1e-9, atol=1e-12)
y_ode = sol.y[0]
ax = plt.subplot(1, 1, 1)
ax.plot(t, x, label='input')
ax.plot(t, y, label='output (h*x)')
ax.plot(t, y_ode, '--', label='output (ODE)', color='r')
ax.grid(True)
ax.set_xlabel('t (s)')
ax.set_ylabel('V')
ax.set_xlim([0, xmax])
ax.set_title(f'Single sinusoid: f = {f:g} Hz')
ax.legend()
xmax = xmax/2
plt.tight_layout()
if (do_plot):
plt.show()
return plt, t, x, y
def plot_single_sinusoids():
# ======================
# 1) Single sinusoids
# ======================
A = 1.0
freqs = [1, 5, 10, 50]
xmax = 2.0
plt.figure(1)
plt.clf()
for k, f in enumerate(freqs, start=1):
w = 2*np.pi*f
x = A * np.sin(w*t) * (t >= 0)
y_full = conv_num(x, h, dt)
y = y_full[:N] # keep same length as x
# ODE: tau1*tau2*y'' + (tau1+tau2)*y' + y = G*x(t)
def rhs(ti, z):
x_t = A*np.sin(w*ti) * (ti >= 0.0)
return [
z[1],
(G*x_t - (tau1 + tau2)*z[1] - z[0]) / (tau1*tau2)
]
z0 = [0.0, 0.0] # zero initial conditions
sol = solve_ivp(rhs, (t[0], t[-1]), z0, t_eval=t, rtol=1e-9, atol=1e-12)
y_ode = sol.y[0]
ax = plt.subplot(len(freqs), 1, k)
ax.plot(t, x, label='input')
ax.plot(t, y, label='output (h*x)')
ax.plot(t, y_ode, '--', label='output (ODE)', color='r')
ax.grid(True)
ax.set_xlabel('t (s)')
ax.set_ylabel('V')
ax.set_xlim([0, xmax])
ax.set_title(f'Single sinusoid: f = {f:g} Hz')
ax.legend()
xmax = xmax/2
plt.tight_layout()
plt.show()
#plot_single_sinusoids()
#exit()
def plot_sum_sinusoids():
# =========================================
# 2) Sum of sinusoids
# =========================================
A1, f1 = 1.0, 5.0
A2, f2 = 0.5, 20.0
x1 = A1 * np.sin(2*np.pi*f1*t) * (t >= 0)
x2 = A2 * np.sin(2*np.pi*f2*t) * (t >= 0)
alpha, beta = 1.5, 2.0
x = alpha*x1 + beta*x2
Nx = x.size
y = conv_num(x, h, dt)[:Nx]
y1 = conv_num(x1, h, dt)[:Nx]
y2 = conv_num(x2, h, dt)[:Nx]
plt.figure(2)
plt.clf()
plt.plot(t, y, label='y = h*(x1+x2)')
plt.plot(t, alpha*y1 + beta*y2, '--', label='alpha*y1 + beta*y2')
plt.grid(True)
plt.xlabel('t (s)')
plt.ylabel('V')
plt.xlim([0, 1])
plt.legend()
plt.title('Linearity check: y[u1+u2] vs y[u1]+y[u2]')
plt.show()
#plot_sum_sinusoids()
#exit()
def plot_time_invariance():
# ==================================
# 3) Time invariance (delay \Delta)
# ==================================
A1, f1 = 1.0, 5.0
A2, f2 = 0.5, 20.0
x1 = A1 * np.sin(2*np.pi*f1*t) * (t >= 0)
x2 = A2 * np.sin(2*np.pi*f2*t) * (t >= 0)
alpha, beta = 1.5, 2.0
x = alpha*x1 + beta*x2
Nx = x.size
Delta = 0.1
nD = int(round(Delta/dt))
x_del = np.concatenate([np.zeros(nD), x[:-nD]]) if nD < Nx else np.zeros_like(x)
y_del = conv_num(x_del, h, dt)[:x_del.size]
y = conv_num(x, h, dt)[:Nx]
plt.figure(3)
plt.clf()
plt.plot(t, y, label='y(t)')
plt.plot(t, np.concatenate([np.zeros(nD), y[:-nD]]), label=r'$y(t-\Delta)$')
plt.plot(t, y_del, '--', label=r'$h*u(t-\Delta)$')
plt.grid(True)
plt.xlabel('t (s)')
plt.ylabel('V')
plt.xlim([0, 1])
plt.legend()
plt.title('Time invariance check')
plt.tight_layout()
plt.show()
#plot_time_invariance()
#exit()
def plot_commutativity():
# =======================================
# 4) Commutativity of stages (h1*h2)
# =======================================
h12 = G * dt * np.convolve(h1, h2, mode='full')
h21 = G * dt * np.convolve(h2, h1, mode='full')
rmse = np.sqrt(np.mean((h12 - h21)**2))
plt.figure(4)
plt.clf()
plt.plot(t, h12[:N], label=r'$h_1 \ast h_2$')
plt.plot(t, h21[:N], '--', label=r'$h_2 \ast h_1$')
plt.grid(True)
plt.xlim([0, 0.1])
plt.xlabel('t (s)')
plt.ylabel('V')
plt.legend()
plt.title(rf'Commutativity - RMSE of $ e = h_{{12}} - h_{{21}}: \sqrt{{\mathrm{{mean}}(e^2)}} = {rmse:.3e} $')
plt.tight_layout()
plt.show()
#plot_commutativity()
#exit()
def load_scope_data(filename):
f = open(filename, 'r', newline='')
reader = csv.reader(f)
# 3. Read the data rows
timearr, v1arr, v2arr = [], [], []
trigger = False
last_last_v1 = 1
last_v1 = 1
for row in reader:
# Check if the row is not empty
if row:
try:
# Extract Time (column index 1) and Voltage (column index 2)
time = float(row[1])
v1 = float(row[2])
v2 = float(row[3])
if v1 > 0.0 and last_v1 <= 0.0 and last_last_v1 <= 0.0:
trigger = True
print ("TRIGGERED")
last_last_v1 = last_v1
last_v1 = v1
if not(trigger):
continue
# Add the pair to our data list
timearr.append(time)
v1arr.append(v1)
v2arr.append(v2)
except (ValueError, IndexError):
# This handles any malformed lines or conversion errors
print(f"Skipping malformed row: {row}")
return np.array(timearr), np.array(v1arr), np.array(v2arr)
def calc_rmse(predicted_t, predicted_v, actual_t, actual_v): # The frame of reference is t1.
actual_v_interp = np.interp(predicted_t, actual_t, actual_v)
# Now, we can directly compare actual_v_interp with predicted_v.
squared_error = (actual_v_interp - predicted_v) ** 2
mse = np.mean(squared_error)
rmse = np.sqrt(mse)
return rmse
def plot_single_sinusoid_freq(freq):
t2, x2, y2 = load_scope_data('/home/hurricos/Sync/org/School/EE3150/LAB3/data/2_2_a/' + str(freq) + 'hz.csv')
t2 += 0 - t2[0]
plt, t, x, y = plot_single_sinusoid(freq, t2[-1], dt, False)
plt.plot(t2, x2, label=r'v_in')
plt.plot(t2, y2, label=r'v_c2')
print("RMSE @ ", str(freq), ": ", calc_rmse(t, y, t2, y2))
plt.legend()
plt.show()
def run_2_2_a_conv_pred():
plot_single_sinusoid_freq(1)
plot_single_sinusoid_freq(5)
plot_single_sinusoid_freq(10)
plot_single_sinusoid_freq(100)
def run_2_2_b_show():
plt.figure(num='Window')
plt.xlabel('t (s)')
plt.title(f'Sum of sinusoids')
plt.ylabel('V')
plt.grid(True)
t2, x2, y2 = load_scope_data('/home/hurricos/Sync/org/School/EE3150/LAB3/data/2_2_b/y1.csv')
plt.plot(t2, x2, label=r'v_in1')
plt.plot(t2, y2, label=r'v_c21')
t2, x2, y2 = load_scope_data('/home/hurricos/Sync/org/School/EE3150/LAB3/data/2_2_b/y2.csv')
plt.plot(t2, x2, label=r'v_in2')
plt.plot(t2, y2, label=r'v_c22')
t2, x2, y2 = load_scope_data('/home/hurricos/Sync/org/School/EE3150/LAB3/data/2_2_b/y1_y2_sum.csv')
plt.plot(t2, x2, label=r'v_in')
plt.plot(t2, y2, label=r'v_c2')
plt.legend()
plt.show()
run_2_2_b_show()
\end{minted}
\section{Summary and Conclusions}
We encountered many difficulties in this lab. The difficulties fell into three categories:
\begin{itemize}
\item Incorrect circuit assembly delayed our ability to collect data, and ultimately hampered
\item In the absence of context for calculations to be done later in our report, the collection of data was not done in a way conducive to consumption of later day (e.g. nonsynchronization)
\item The calculations themselves were difficult to do, and ultimately consumed the bulk of the time we had available to complete the lab report.
\end{itemize}
Nevertheless, we were able to exercise techniques which process signal
data, and review and confirm \textit{at least} that the signal
responses of sinusoidal inputs were also sinusoidal, and shared the
same frequency.
\end{document}