Wave-Particle Duality¶
Elijah Renner
Imports (run first):
!pip3 install ipython numpy matplotlib
Requirement already satisfied: ipython in /srv/conda/envs/notebook/lib/python3.10/site-packages (8.32.0) Collecting numpy Downloading numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (62 kB) Collecting matplotlib Downloading matplotlib-3.10.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (11 kB) Requirement already satisfied: decorator in /srv/conda/envs/notebook/lib/python3.10/site-packages (from ipython) (5.1.1) Requirement already satisfied: exceptiongroup in /srv/conda/envs/notebook/lib/python3.10/site-packages (from ipython) (1.2.2) Requirement already satisfied: jedi>=0.16 in /srv/conda/envs/notebook/lib/python3.10/site-packages (from ipython) (0.19.2) Requirement already satisfied: matplotlib-inline in /srv/conda/envs/notebook/lib/python3.10/site-packages (from ipython) (0.1.7) Requirement already satisfied: pexpect>4.3 in /srv/conda/envs/notebook/lib/python3.10/site-packages (from ipython) (4.9.0) Requirement already satisfied: prompt_toolkit<3.1.0,>=3.0.41 in /srv/conda/envs/notebook/lib/python3.10/site-packages (from ipython) (3.0.50) Requirement already satisfied: pygments>=2.4.0 in /srv/conda/envs/notebook/lib/python3.10/site-packages (from ipython) (2.19.1) Requirement already satisfied: stack_data in /srv/conda/envs/notebook/lib/python3.10/site-packages (from ipython) (0.6.3) Requirement already satisfied: traitlets>=5.13.0 in /srv/conda/envs/notebook/lib/python3.10/site-packages (from ipython) (5.14.3) Requirement already satisfied: typing_extensions>=4.6 in /srv/conda/envs/notebook/lib/python3.10/site-packages (from ipython) (4.12.2) Collecting contourpy>=1.0.1 (from matplotlib) Downloading contourpy-1.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (5.5 kB) Collecting cycler>=0.10 (from matplotlib) Downloading cycler-0.12.1-py3-none-any.whl.metadata (3.8 kB) Collecting fonttools>=4.22.0 (from matplotlib) Downloading fonttools-4.58.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl.metadata (104 kB) Collecting kiwisolver>=1.3.1 (from matplotlib) Downloading kiwisolver-1.4.8-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl.metadata (6.2 kB) Requirement already satisfied: packaging>=20.0 in /srv/conda/envs/notebook/lib/python3.10/site-packages (from matplotlib) (24.2) Collecting pillow>=8 (from matplotlib) Downloading pillow-11.2.1-cp310-cp310-manylinux_2_28_x86_64.whl.metadata (8.9 kB) Collecting pyparsing>=2.3.1 (from matplotlib) Downloading pyparsing-3.2.3-py3-none-any.whl.metadata (5.0 kB) Requirement already satisfied: python-dateutil>=2.7 in /srv/conda/envs/notebook/lib/python3.10/site-packages (from matplotlib) (2.9.0.post0) Requirement already satisfied: parso<0.9.0,>=0.8.4 in /srv/conda/envs/notebook/lib/python3.10/site-packages (from jedi>=0.16->ipython) (0.8.4) Requirement already satisfied: ptyprocess>=0.5 in /srv/conda/envs/notebook/lib/python3.10/site-packages (from pexpect>4.3->ipython) (0.7.0) Requirement already satisfied: wcwidth in /srv/conda/envs/notebook/lib/python3.10/site-packages (from prompt_toolkit<3.1.0,>=3.0.41->ipython) (0.2.13) Requirement already satisfied: six>=1.5 in /srv/conda/envs/notebook/lib/python3.10/site-packages (from python-dateutil>=2.7->matplotlib) (1.17.0) Requirement already satisfied: executing>=1.2.0 in /srv/conda/envs/notebook/lib/python3.10/site-packages (from stack_data->ipython) (2.1.0) Requirement already satisfied: asttokens>=2.1.0 in /srv/conda/envs/notebook/lib/python3.10/site-packages (from stack_data->ipython) (3.0.0) Requirement already satisfied: pure_eval in /srv/conda/envs/notebook/lib/python3.10/site-packages (from stack_data->ipython) (0.2.3) Downloading numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (16.8 MB) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 16.8/16.8 MB 64.2 MB/s eta 0:00:0000:01 Downloading matplotlib-3.10.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (8.6 MB) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 8.6/8.6 MB 109.6 MB/s eta 0:00:00 Downloading contourpy-1.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (325 kB) Downloading cycler-0.12.1-py3-none-any.whl (8.3 kB) Downloading fonttools-4.58.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (4.7 MB) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 4.7/4.7 MB 157.4 MB/s eta 0:00:00 Downloading kiwisolver-1.4.8-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl (1.6 MB) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.6/1.6 MB 118.8 MB/s eta 0:00:00 Downloading pillow-11.2.1-cp310-cp310-manylinux_2_28_x86_64.whl (4.6 MB) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 4.6/4.6 MB 152.1 MB/s eta 0:00:00 Downloading pyparsing-3.2.3-py3-none-any.whl (111 kB) Installing collected packages: pyparsing, pillow, numpy, kiwisolver, fonttools, cycler, contourpy, matplotlib Successfully installed contourpy-1.3.2 cycler-0.12.1 fonttools-4.58.0 kiwisolver-1.4.8 matplotlib-3.10.3 numpy-2.2.6 pillow-11.2.1 pyparsing-3.2.3
Background¶
Electromagnetic Waves and their Vector Fields¶
Electromagnetic waves are waves that are formed when an electric field interacts with a magnetic field.
from IPython.display import Image, display
display(Image(filename='em_wave.jpeg'))
We can see that each point on this wave has a magnetic and electric field vector at each point $(x,y,z)$. Mathematically, we define these vector fields as
$$\mathbf{E} = \begin{pmatrix}E_x\\[2pt]E_y\\[2pt]E_z\end{pmatrix}, \qquad \mathbf{B} = \begin{pmatrix}B_x\\[2pt]B_y\\[2pt]B_z\end{pmatrix}. $$
The unit for the electric field is $\frac{V}{m}$ or force per unit charge. The magnetic field is in teslas $(T)$, which quantify magnetic flux density, or how many “magnetic field-lines” pierce a tiny patch, and with what orientation.
Calculus Concepts¶
It's helpful to understand a few operations. First we cover the curl. The curl measures local spin in a vector field. Take, for example, a 2D vector field with a spinning shape like
import matplotlib.pyplot as plt
import numpy as np
x, y = np.meshgrid(np.linspace(-3, 3, 20), np.linspace(-3, 3, 20))
u = -y
v = x
fig, ax = plt.subplots(figsize=(6,6))
ax.quiver(x, y, u, v, color='royalblue', pivot='mid')
ax.set_title("Spinning 2D Vector Field", pad=12)
ax.set_xlabel("x")
ax.set_ylabel("y")
ax.set_aspect('equal')
circle = plt.Circle((0, 0), 0.4, color='k', fill=False, linewidth=2)
ax.add_patch(circle)
ax.plot(0, 0, marker='o', markersize=10, color='k', markerfacecolor='w', markeredgewidth=2)
ax.plot(0, 0, marker='.', markersize=10, color='k') # dot in the center
plt.show()
Now, imagine placing a floating object in the center. It will spin counterclockwise around an axis at $(x,y)=(0,0)$ as shown by the black dot protruding through the screen. That spin axis is the direction of the curl. So, at the highest level, the curl allows us to measure the "twist" content of a vector field by converting qualitative swirling observations into a precise vector quantity.
The curl:
“how, and around which axis, is this vector field trying to rotate things right here?”
A more formal Cartesian definition of the curl is that a vector field $\mathbf{F}=\begin{pmatrix}F_x\\[2pt]F_y\\[2pt]F_z\end{pmatrix}$ has the curl $$\nabla \times \mathbf{F}= \begin{pmatrix} \dfrac{\partial F_z}{\partial y} - \dfrac{\partial F_y}{\partial z}\\[6pt] \dfrac{\partial F_x}{\partial z} - \dfrac{\partial F_z}{\partial x}\\[6pt] \dfrac{\partial F_y}{\partial x} - \dfrac{\partial F_x}{\partial y} \end{pmatrix}.$$
Wave Model¶
Faraday's Law¶
Faraday's Law states that $$\nabla \times \mathbf{E}=-\frac{\partial \mathbf{B}}{\partial t}$$
The LHS (curl of $\mathbf{E}$) measures how strongly the electric field is swirling at each point. The RHS is the negative rate at which the magnetic field's flux density is changing at that point is changing in time.
Essentially, a magnetic field that is changing in time $\left(\frac{\partial \mathbf{B}}{\partial t}\neq0\right)$ creates a circulating electric field whose swirl strength exactly matches that time-change, with a direction that opposes the change.
The following demonstration gives some intuition for how the changing magnetic field induces an electric field with arbitrary data:
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.animation import FuncAnimation
from IPython.display import HTML
# ---------- physical parameters ----------
B0, freq = 0.10, 1.0 # Tesla, Hz
ω = 2 * np.pi * freq # angular frequency (rad/s)
# ---------- grid for E-field ----------
rmax, N = 1.0, 19 # metres, grid resolution
x = np.linspace(-rmax, rmax, N)
y = np.linspace(-rmax, rmax, N)
X, Y = np.meshgrid(x, y)
# ---------- time base ----------
fps = 30 # frames per second
cycles = 2
frames_per_cycle = 60 # frames per cycle
frames_total = cycles * frames_per_cycle
t_series = np.arange(frames_total) / fps
B_series = B0 * np.sin(ω * t_series)
dBdt_series = B0 * ω * np.cos(ω * t_series)
# ---------- figure layout ----------
fig, (ax_field, ax_time) = plt.subplots(
2, 1, figsize=(8, 8),
gridspec_kw={'height_ratios': [4, 1]}
)
fig.subplots_adjust(hspace=0.4)
fig.suptitle(
'Top: Induced electric field E swirling per Faraday’s law\n'
'Bottom: Magnetic field B(t) and rate of change dB/dt vs time',
fontsize=14,
y=0.97
)
# top panel: induced E field
ax_field.set(aspect='equal', xlim=(-rmax, rmax), ylim=(-rmax, rmax),
xlabel=r'$x\ (\mathrm{m})$', ylabel=r'$y\ (\mathrm{m})$')
ax_field.grid(alpha=0.3)
quiv = ax_field.quiver(X, Y, np.zeros_like(X), np.zeros_like(Y),
scale_units='xy', scale=1, width=0.005, color='teal')
txt_vals = ax_field.text(0.5, 1.02, '', transform=ax_field.transAxes,
ha='center', va='bottom', fontsize=10)
# bottom panel: B(t) and dB/dt traces
ax_time.plot(t_series, B_series, label=r'$B(t)$')
ax_time.plot(t_series, dBdt_series, label=r'$dB/dt$')
cursor = ax_time.axvline(0, ls='--', lw=1, color='k')
ax_time.set(xlabel='Time (s)', ylabel='Value')
ax_time.legend(loc='upper right')
ax_time.grid(alpha=0.3)
# ---------- animation update ----------
def update(frame):
dBdt = dBdt_series[frame]
Ex = 0.5 * dBdt * Y
Ey = -0.5 * dBdt * X
quiv.set_UVC(Ex, Ey)
t = t_series[frame]
B = B_series[frame]
txt_vals.set_text(
fr'$t={t:.2f}\,\mathrm{{s}}\;\;B={B:+.3f}\,\mathrm{{T}}\;\;dB/dt={dBdt:+.3f}\,\mathrm{{T/s}}$'
)
cursor.set_xdata([t, t])
return quiv, cursor, txt_vals
# ---------- run animation ----------
ani = FuncAnimation(fig, update, frames=frames_total,
interval=1000/fps, blit=True)
display(HTML(ani.to_jshtml()))
plt.close(fig) # Prevents a static frame from being shown after the animation
Animation size has reached 21041992 bytes, exceeding the limit of 20971520.0. If you're sure you want a larger animation embedded, set the animation.embed_limit rc parameter to a larger value (in MB). This and further frames will be dropped.