-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathq2_method_of_images.py
More file actions
331 lines (273 loc) · 12.3 KB
/
q2_method_of_images.py
File metadata and controls
331 lines (273 loc) · 12.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
"""
q2_method_of_images.py
======================
EE1204 Simulation Assignment 1 — Question 2
Point Charge Near a Grounded Conducting Sphere (2D)
Uses the **method of images** with the 2D logarithmic potential on a
300×300 grid. The grounded sphere (radius R = 2) is centred at the origin
and the point charge Q = 10 µC is placed at (d, 0) = (4, 0).
Image-charge formulas (for a grounded cylinder of radius R):
Image position : d' = R² / d
Image charge : Q' = −Q R / d
Figures produced (saved to ./figures/q2/):
fig1 — Equipotential contours (base case)
fig2 — Electric field vectors (quiver, log-coloured)
fig3 — Induced surface charge density σ(θ) (base case)
fig4 — σ(θ) for varying charge magnitudes Q
fig5 — σ(θ) for varying distances d
fig6 — Equipotential panels for 4 distances
fig7 — Electric field streamlines (base case)
Author : Aniket (EE25BTECH11007)
Course : EE1204 — Engineering Electromagnetics, IIT Hyderabad
Date : March 2026
"""
import numpy as np
import matplotlib.pyplot as plt
import matplotlib as mpl
import os
mpl.use('Agg')
from scipy.interpolate import RegularGridInterpolator
import style # noqa: F401,E402
# ═══════════════════════════════════════════════════════════════
# 1. PARAMETERS
# ═══════════════════════════════════════════════════════════════
R = 2.0 # sphere (cylinder) radius [simulation units]
Q0 = 10e-6 # base-case charge magnitude = 10 µC
d0 = 4.0 # base-case charge position x = 4
# Simulation domain
XMIN, XMAX = -6.0, 6.0
YMIN, YMAX = -6.0, 6.0
NG = 300 # grid resolution → 300 × 300
delta = (XMAX - XMIN) / (NG - 1) # grid spacing ≈ 0.04
# Grid arrays
x = np.linspace(XMIN, XMAX, NG)
y = np.linspace(YMIN, YMAX, NG)
X, Y = np.meshgrid(x, y)
os.makedirs('figures/q2', exist_ok=True)
# ═══════════════════════════════════════════════════════════════
# 2. METHOD OF IMAGES — CORE FUNCTIONS
# ═══════════════════════════════════════════════════════════════
def image_params(Q, d):
"""
Compute the image charge and its position for a line charge Q
at distance d from the axis of a grounded cylinder of radius R.
Returns
-------
Q_img : float — image charge magnitude Q' = −Q R / d
d_img : float — image position d' = R² / d
"""
Q_img = -Q * R / d
d_img = R**2 / d
return Q_img, d_img
def potential(X, Y, Q, d):
"""
2D logarithmic potential at every grid point (X, Y) due to a real
charge Q at (d, 0) and its image inside the sphere.
V = −Q ln(r_q) − Q' ln(r_q')
The potential inside the sphere is set to zero (conductor interior).
"""
Q_img, d_img = image_params(Q, d)
# Distance from each grid point to the real charge
r_real = np.maximum(np.hypot(X - d, Y), 1e-15)
# Distance from each grid point to the image charge
r_img = np.maximum(np.hypot(X - d_img, Y), 1e-15)
V = -Q * np.log(r_real) - Q_img * np.log(r_img)
# Enforce V = 0 inside the conductor
V[X**2 + Y**2 <= R**2] = 0.0
return V
def electric_field(X, Y, Q, d):
"""
Analytical electric field at every grid point from the real charge
at (d, 0) and its image.
For a line charge at (x₀, y₀), the field contribution is:
E_x = Q (x − x₀) / r²
E_y = Q (y − y₀) / r²
"""
Q_img, d_img = image_params(Q, d)
# Real charge contribution
dx1, dy1 = X - d, Y
r1sq = np.maximum(dx1**2 + dy1**2, 1e-15)
# Image charge contribution
dx2, dy2 = X - d_img, Y
r2sq = np.maximum(dx2**2 + dy2**2, 1e-15)
Ex = Q * dx1 / r1sq + Q_img * dx2 / r2sq
Ey = Q * dy1 / r1sq + Q_img * dy2 / r2sq
# Zero field inside the conductor
mask = X**2 + Y**2 <= R**2
Ex[mask] = 0.0
Ey[mask] = 0.0
return Ex, Ey
def surface_charge(Q, d, V_grid, n_pts=360):
"""
Numerical surface-charge density on the sphere, computed from
the radial derivative of V at r = R:
σ(θ) = −(V_outside − V_inside) / (2 Δr)
Uses bilinear interpolation on the potential grid.
Parameters
----------
Q, d : charge parameters (used only to label; V_grid already computed)
V_grid : 2D potential array (N × N)
n_pts : number of angular sample points
Returns
-------
theta : array of angles [0, 2π)
sigma : surface-charge density at each angle
"""
interp = RegularGridInterpolator(
(y, x), V_grid, method='linear',
bounds_error=False, fill_value=0.0
)
dr = delta # radial step = one grid spacing
theta = np.linspace(0, 2 * np.pi, n_pts, endpoint=False)
# Sample points just outside and just inside the sphere
pts_outside = np.column_stack([
(R + dr) * np.sin(theta), # y-coordinate
(R + dr) * np.cos(theta) # x-coordinate
])
pts_inside = np.column_stack([
(R - dr) * np.sin(theta),
(R - dr) * np.cos(theta)
])
# σ ≈ −∂V/∂r via central difference
sigma = -(interp(pts_outside) - interp(pts_inside)) / (2 * dr)
return theta, sigma
# ═══════════════════════════════════════════════════════════════
# 3. BASE-CASE COMPUTATION
# ═══════════════════════════════════════════════════════════════
V0 = potential(X, Y, Q0, d0)
Ex0, Ey0 = electric_field(X, Y, Q0, d0)
Em0 = np.hypot(Ex0, Ey0)
# Circle for drawing the sphere boundary
tc = np.linspace(0, 2 * np.pi, 300)
cx, cy = R * np.cos(tc), R * np.sin(tc)
def draw_sphere(ax):
"""Fill the sphere as a grey disc with a black outline."""
ax.fill(cx, cy, color='#d9d9d9', ec='k', lw=1.2, zorder=5)
levs = np.linspace(-8, 8, 33) # contour levels
# ═══════════════════════════════════════════════════════════════
# 4. FIGURE GENERATION
# ═══════════════════════════════════════════════════════════════
# ── Fig 1 : Equipotential contours (base case) ──
fig, ax = plt.subplots(figsize=(7.5, 6.5))
cf = ax.contourf(X, Y, np.clip(V0, -8, 8), levels=levs,
cmap=style.CMAP_POT, extend='both')
ax.contour(X, Y, np.clip(V0, -8, 8), levels=levs,
colors='k', linewidths=0.2, alpha=0.3)
draw_sphere(ax)
ax.plot(d0, 0, '*', color=style.PLATE_NEG, ms=14, zorder=6,
label=f'$Q = {Q0*1e6:.0f}\\;\\mu$C')
fig.colorbar(cf, ax=ax, label='Potential $V$ (normalised)',
shrink=0.82, pad=0.02)
ax.set_xlabel('$x$'); ax.set_ylabel('$y$')
ax.set_title(f'Equipotential Contours '
f'($Q = {Q0*1e6:.0f}\\;\\mu$C at $d = {d0:.0f}$, $R = {R:.0f}$)')
ax.legend(fontsize=10, loc='upper left', framealpha=0.9)
ax.set_aspect('equal'); ax.set_xlim(XMIN, XMAX); ax.set_ylim(YMIN, YMAX)
fig.savefig('figures/q2/fig1_equipotential_base.png')
plt.close()
# ── Fig 2 : Field vectors (quiver, log-coloured) ──
fig, ax = plt.subplots(figsize=(7.5, 6.5))
sk = 8 # skip factor for quiver arrows
Xs, Ys = X[::sk, ::sk], Y[::sk, ::sk]
Exs, Eys = Ex0[::sk, ::sk], Ey0[::sk, ::sk]
Es = np.hypot(Exs, Eys)
Es_safe = np.maximum(Es, 1e-20)
q = ax.quiver(Xs, Ys, Exs / Es_safe, Eys / Es_safe,
np.log10(Es_safe), cmap='YlOrRd',
scale=28, width=0.004, alpha=0.85)
fig.colorbar(q, ax=ax, label=r'$\log_{10}|\mathbf{E}|$',
shrink=0.82, pad=0.02)
draw_sphere(ax)
ax.plot(d0, 0, '*', color=style.PLATE_NEG, ms=14, zorder=6,
label=f'$Q = {Q0*1e6:.0f}\\;\\mu$C')
ax.set_xlabel('$x$'); ax.set_ylabel('$y$')
ax.set_title('Electric Field Distribution (normalised arrows, log-coloured)')
ax.legend(fontsize=10); ax.set_aspect('equal')
ax.set_xlim(XMIN, XMAX); ax.set_ylim(YMIN, YMAX)
fig.savefig('figures/q2/fig2_field_vectors.png')
plt.close()
# ── Fig 3 : Surface charge density σ(θ) (base case) ──
th_s, sig_s = surface_charge(Q0, d0, V0)
fig, ax = plt.subplots(figsize=(7.5, 4.5))
ax.fill_between(np.degrees(th_s), sig_s, 0, where=sig_s < 0,
color='#ef8a62', alpha=0.45, label=r'Negative $\sigma$')
ax.fill_between(np.degrees(th_s), sig_s, 0, where=sig_s >= 0,
color='#67a9cf', alpha=0.45, label=r'Positive $\sigma$')
ax.plot(np.degrees(th_s), sig_s, 'k-', lw=0.7)
ax.axhline(0, color=style.NEUTRAL, lw=0.4)
ax.set_xlabel(r'$\theta$ (degrees from $+x$ axis)')
ax.set_ylabel(r'$\sigma$ (normalised)')
ax.set_title(f'Induced Surface Charge Density '
f'($Q = {Q0*1e6:.0f}\\;\\mu$C, $d = {d0:.0f}$)')
ax.legend(fontsize=9); ax.grid(True)
fig.savefig('figures/q2/fig3_surface_charge_base.png')
plt.close()
# ── Fig 4 : Varying Q ──
Qs = [1e-6, 5e-6, 10e-6, 20e-6]
colours_q = ['#4575b4', '#74add1', '#f46d43', '#d73027']
fig, ax = plt.subplots(figsize=(7.5, 4.5))
for Qv, c in zip(Qs, colours_q):
Vt = potential(X, Y, Qv, d0)
th, sig = surface_charge(Qv, d0, Vt)
ax.plot(np.degrees(th), sig, color=c, lw=1.3,
label=f'$Q = {Qv*1e6:.0f}\\;\\mu$C')
ax.set_xlabel(r'$\theta$ (degrees)')
ax.set_ylabel(r'$\sigma$ (normalised)')
ax.set_title(f'Surface Charge Density — Varying Charge Magnitude '
f'($d = {d0:.0f}$)')
ax.legend(fontsize=9); ax.grid(True)
fig.savefig('figures/q2/fig4_sigma_vary_Q.png')
plt.close()
# ── Fig 5 : Varying d ──
ds = [2.5, 3.0, 4.0, 5.5]
colours_d = ['#762a83', '#af8dc3', '#e7d4e8', '#d9f0d3']
fig, ax = plt.subplots(figsize=(7.5, 4.5))
for dv, c in zip(ds, colours_d):
Vt = potential(X, Y, Q0, dv)
th, sig = surface_charge(Q0, dv, Vt)
ax.plot(np.degrees(th), sig, color=c, lw=1.3,
label=f'$d = {dv:.1f}$')
ax.set_xlabel(r'$\theta$ (degrees)')
ax.set_ylabel(r'$\sigma$ (normalised)')
ax.set_title(f'Surface Charge Density — Varying Distance '
f'($Q = {Q0*1e6:.0f}\\;\\mu$C)')
ax.legend(fontsize=9); ax.grid(True)
fig.savefig('figures/q2/fig5_sigma_vary_d.png')
plt.close()
# ── Fig 6 : Equipotential panels for 4 distances ──
fig, axes = plt.subplots(2, 2, figsize=(12, 10))
for ax, dv in zip(axes.flat, ds):
Vv = np.clip(potential(X, Y, Q0, dv), -8, 8)
cf = ax.contourf(X, Y, Vv, levels=levs, cmap=style.CMAP_POT,
extend='both')
ax.contour(X, Y, Vv, levels=levs, colors='k',
linewidths=0.15, alpha=0.25)
draw_sphere(ax)
ax.plot(dv, 0, '*', color=style.PLATE_NEG, ms=11, zorder=6)
ax.set_title(f'$d = {dv:.1f}$', fontsize=12)
ax.set_aspect('equal')
ax.set_xlim(XMIN, XMAX); ax.set_ylim(YMIN, YMAX)
fig.colorbar(cf, ax=ax, shrink=0.78, pad=0.02)
fig.suptitle(f'Equipotential Maps for Varying Charge Distance '
f'($Q = {Q0*1e6:.0f}\\;\\mu$C)', fontsize=14, y=1.01)
plt.tight_layout()
fig.savefig('figures/q2/fig6_equipotential_vary_d.png')
plt.close()
# ── Fig 7 : Streamlines (base case) ──
fig, ax = plt.subplots(figsize=(7.5, 6.5))
cf = ax.contourf(X, Y, np.clip(V0, -8, 8), levels=levs,
cmap=style.CMAP_POT, alpha=0.30, extend='both')
lnorm = mpl.colors.LogNorm(vmin=1e-8,
vmax=Em0[np.isfinite(Em0)].max())
ax.streamplot(X, Y, Ex0, Ey0, color=Em0, cmap=style.CMAP_FIELD,
norm=lnorm, density=2.2, linewidth=0.65, arrowsize=0.75)
draw_sphere(ax)
ax.plot(d0, 0, '*', color=style.PLATE_NEG, ms=14, zorder=6)
fig.colorbar(cf, ax=ax, label='$V$ (normalised)', shrink=0.82, pad=0.02)
ax.set_xlabel('$x$'); ax.set_ylabel('$y$')
ax.set_title('Electric Field Streamlines (base case)')
ax.set_aspect('equal')
ax.set_xlim(XMIN, XMAX); ax.set_ylim(YMIN, YMAX)
fig.savefig('figures/q2/fig7_streamlines.png')
plt.close()
print("Q2 (Method of Images) complete — 7 figures saved to figures/q2/")