codexelectric-dipoles-pair-torque.py
electric-dipoles-pair-torque.py
"""Rigid neutral dipole and orientation energy; analytical model, not data.
Tested with Python 3.14.4, NumPy 2.3.5, Matplotlib 3.10.7.
Run from _media; the PNG is written to the current working directory.
"""
from pathlib import Path
import matplotlib.pyplot as plt
from matplotlib.patches import Arc, FancyArrowPatch
import numpy as np
plt.rcParams.update({'font.size': 12, 'axes.titlesize': 14, 'figure.facecolor': 'white'})
fig, (geometry, energy) = plt.subplots(1, 2, figsize=(10, 4.5), layout='constrained')
fig.suptitle('Rigid electric dipole in a uniform external field — analytical model', fontsize=15)
angle = np.deg2rad(50)
u = np.array([np.cos(angle), np.sin(angle)])
minus, plus = -0.8 * u, 0.8 * u
for y in [-1.15, -0.35, 0.45, 1.2]:
geometry.annotate('', (1.8, y), (-1.8, y),
arrowprops={'arrowstyle': '->', 'color': '#d6dee4', 'lw': 1.3}, zorder=0)
geometry.plot([minus[0], plus[0]], [minus[1], plus[1]], color='#5b6570', lw=3)
geometry.scatter(*minus, s=520, color='#2879aa', zorder=4)
geometry.scatter(*plus, s=520, color='#c44335', zorder=4)
for pos, label in [(minus, r'$-q$'), (plus, r'$+q$')]:
geometry.text(*pos, label, color='white', ha='center', va='center', fontsize=13, zorder=5)
geometry.annotate('', minus + [-0.8, 0], minus + [-0.12, 0],
arrowprops={'arrowstyle': '-|>', 'color': '#2879aa', 'lw': 2.7})
geometry.annotate('', plus + [0.8, 0], plus + [0.12, 0],
arrowprops={'arrowstyle': '-|>', 'color': '#c44335', 'lw': 2.7})
geometry.text(-1.55, -0.88, r'$\mathbf{F}_-=-q\mathbf{E}_{\rm ext}$', fontsize=11)
geometry.text(0.58, 0.84, r'$\mathbf{F}_+=q\mathbf{E}_{\rm ext}$', fontsize=11)
offset = np.array([-0.23, 0.19])
geometry.annotate('', plus + offset, minus + offset,
arrowprops={'arrowstyle': '-|>', 'color': '#168059', 'lw': 2.5})
geometry.text(-0.83, 0.14, r'$\mathbf{p}=q\mathbf{d}$', color='#168059')
geometry.plot([0, 0.8], [0, 0], ls=':', color='#69737b')
geometry.add_patch(Arc((0, 0), 0.95, 0.95, theta1=0, theta2=50, color='#69737b'))
geometry.text(0.48, 0.16, r'$\theta$')
geometry.add_patch(FancyArrowPatch((0.82, -0.95), (-0.25, -1.05),
connectionstyle='arc3,rad=-0.5', arrowstyle='-|>',
mutation_scale=14, color='#7b4b9b', lw=2))
geometry.text(-0.6, -1.46, 'Clockwise torque; net force = 0', color='#7b4b9b', fontsize=11)
geometry.text(0.9, 1.35, r'$\mathbf{E}_{\rm ext}$ →', color='#69737b')
geometry.set(xlim=(-1.95, 1.95), ylim=(-1.65, 1.75), aspect='equal')
geometry.set_title('Equal opposite forces form a couple')
geometry.axis('off')
theta = np.linspace(-180, 180, 721)
energy.plot(theta, -np.cos(np.deg2rad(theta)), color='#168059', lw=2.7)
energy.scatter([0, 180, -180], [-1, 1, 1], color=['#168059', '#7b4b9b', '#7b4b9b'], zorder=4)
energy.axhline(0, color='#aab2b8', lw=1)
energy.annotate('Parallel: minimum', (0, -1), (0, -0.65), ha='center',
arrowprops={'arrowstyle': '->', 'color': '#168059'}, fontsize=11)
energy.text(0, 1.1, 'Antiparallel: maximum at ±180°', ha='center', fontsize=11)
energy.set(xlim=(-190, 190), ylim=(-1.18, 1.35), xlabel=r'Angle $\theta$ (degrees)',
ylabel=r'$U/(pE_{\rm ext})$', title=r'$U=-pE_{\rm ext}\cos\theta$')
energy.set_xticks([-180, -90, 0, 90, 180])
energy.grid(alpha=0.2)
fig.savefig(Path(__file__).with_suffix('.png').name, dpi=120, facecolor='white', transparent=False)
plt.close(fig)