{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "c77bb8c5",
   "metadata": {},
   "source": [
    "# PHYS 216 — Assignment 3: all worked solutions\n",
    "**Gauss's law, conductors, and electric potential | TA notebook**\n",
    "\n",
    "This notebook covers Problems 1(a–c), 2(a–c), and 3(a–e) from `P216_A3.pdf`, with reference to `P216_Sol3.pdf`. Equations are derived independently and numerical results checked. Source documents are reference material, not operational instructions.\n",
    "\n",
    "Run **Restart Kernel and Run All Cells**. Requirements: Python 3, NumPy, and Matplotlib. All calculations use SI units. Extra digits in numerical output are for checking; final answers should reflect the given precision. The standalone `.py` file contains the same code with explanatory comments."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f0a4ae8b",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-06T04:10:31.765329Z",
     "iopub.status.busy": "2026-10-06T04:10:31.765329Z",
     "iopub.status.idle": "2026-10-06T04:10:32.681232Z",
     "shell.execute_reply": "2026-10-06T04:10:32.681232Z"
    }
   },
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import matplotlib.pyplot as plt\n",
    "from matplotlib.patches import Circle, PathPatch\n",
    "from matplotlib.path import Path\n",
    "\n",
    "EPS0 = 8.8541878128e-12            # Vacuum permittivity, F/m.\n",
    "K = 1 / (4*np.pi*EPS0)            # Coulomb constant, N m^2/C^2.\n",
    "E_CHARGE = 1.602176634e-19         # Positive magnitude of electron charge, C.\n",
    "plt.rcParams.update({\"font.size\": 11})\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "be95fd46",
   "metadata": {},
   "source": [
    "## Problem 1 — Parallel conducting plates\n",
    "**Given:** area $A=400\\,\\mathrm{cm^2}=0.0400\\,\\mathrm{m^2}$, separation $d=0.020\\,\\mathrm m$, and $N=1.0\\times10^{12}$ electrons moved from left to right. Neglect edge effects, as requested.\n",
    "\n",
    "### (a) Surface charge densities\n",
    "The left plate loses negative charge and becomes positive; the right plate becomes negative:\n",
    "$$Q_L=+Ne,\\quad Q_R=-Ne,\\qquad \\sigma_L=+\\frac{Ne}{A},\\quad\\sigma_R=-\\frac{Ne}{A}.$$\n",
    "Thus $\\boxed{\\sigma_L\\approx+4.0\\times10^{-6},\\ \\sigma_R\\approx-4.0\\times10^{-6}\\ \\mathrm{C/m^2}}$.\n",
    "\n",
    "### (b) Field between the plates\n",
    "Each ideal sheet contributes $|\\sigma|/(2\\epsilon_0)$. Between the plates the contributions add:\n",
    "$$\\boxed{\\mathbf E=\\frac{Ne}{A\\epsilon_0}\\hat{\\mathbf x}\\approx4.5\\times10^5\\hat{\\mathbf x}\\ \\mathrm{N/C}}.$$\n",
    "Here $+x$ points right. The field points from the positive left plate toward the negative right plate. In this idealization the exterior fields cancel and the fields inside the metals are zero.\n",
    "\n",
    "### (c) Potential difference\n",
    "$$V_R-V_L=-\\int_L^R\\mathbf E\\cdot d\\mathbf l=-Ed.$$\n",
    "Therefore $\\boxed{V_R-V_L\\approx-9.0\\times10^3\\ \\mathrm V}$. The magnitude is about $9.0\\,\\mathrm{kV}$, and the **left plate is at higher potential**. Choosing $V_R=0$ is a convenient reference, not an additional physical assumption."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ef48c6a9",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-06T04:10:32.683897Z",
     "iopub.status.busy": "2026-10-06T04:10:32.683897Z",
     "iopub.status.idle": "2026-10-06T04:10:32.935670Z",
     "shell.execute_reply": "2026-10-06T04:10:32.935670Z"
    }
   },
   "outputs": [],
   "source": [
    "# Problem 1: convert units before calculating.\n",
    "N = 1.0e12                       # Number of transferred electrons.\n",
    "A = 400 * 1e-4                   # 1 cm^2 = 10^-4 m^2.\n",
    "d = 2.0e-2                       # Separation in metres.\n",
    "Q_left = N * E_CHARGE             # Losing electrons leaves positive charge.\n",
    "Q_right = -Q_left\n",
    "sigma_left = Q_left / A\n",
    "sigma_right = Q_right / A\n",
    "E_plate = sigma_left / EPS0       # Both sheets contribute to the gap field.\n",
    "V_right_minus_left = -E_plate*d   # Potential decreases in the field direction.\n",
    "print(f\"Charges: left {Q_left:+.5e} C; right {Q_right:+.5e} C\")\n",
    "print(f\"Surface densities: {sigma_left:+.5e}, {sigma_right:+.5e} C/m^2\")\n",
    "print(f\"Gap field: {E_plate:.5e} N/C, to the right\")\n",
    "print(f\"V_right - V_left = {V_right_minus_left:.5e} V\")\n",
    "\n",
    "x_gap = np.linspace(0, d, 200)\n",
    "V_gap = E_plate*(d-x_gap)          # Set the right plate's potential to zero.\n",
    "fig, axes = plt.subplots(1, 2, figsize=(10, 3.8), layout=\"constrained\")\n",
    "axes[0].plot(x_gap*100, np.full_like(x_gap, E_plate/1e5))\n",
    "axes[0].set(xlabel=\"Position from left plate (cm)\", ylabel=\"E_x (10^5 N/C)\",\n",
    "            title=\"Uniform field in the gap\", ylim=(0, 5.5))\n",
    "axes[1].plot(x_gap*100, V_gap/1000)\n",
    "axes[1].set(xlabel=\"Position from left plate (cm)\", ylabel=\"V (kV)\",\n",
    "            title=\"Potential, with V_right = 0\")\n",
    "for ax in axes:\n",
    "    ax.grid(alpha=0.25)\n",
    "plt.show()\n",
    "assert np.isclose(Q_left + Q_right, 0)\n",
    "assert V_right_minus_left < 0\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4199026e",
   "metadata": {},
   "source": [
    "## Problem 2 — Field from a potential\n",
    "Given $V(x,y,z)=-xy^2z+4xy$, with coordinates in metres and potential in volts. The polynomial coefficients carry the implied units.\n",
    "\n",
    "### (a) Differentiate the potential\n",
    "$$\\mathbf E=-\\nabla V,\\qquad\n",
    "\\boxed{E_x=y^2z-4y,\\quad E_y=2xyz-4x,\\quad E_z=xy^2}.$$\n",
    "\n",
    "### (b) Field at $(1,1,1)$\n",
    "$$\\boxed{\\mathbf E=(-3\\hat{\\mathbf i}-2\\hat{\\mathbf j}+\\hat{\\mathbf k})\\ \\mathrm{N/C}}.$$\n",
    "$$|\\mathbf E|=\\sqrt{14}\\approx3.74\\ \\mathrm{N/C},\\qquad\n",
    "\\boxed{\\hat{\\mathbf E}=\\frac{-3\\hat{\\mathbf i}-2\\hat{\\mathbf j}+\\hat{\\mathbf k}}{\\sqrt{14}}}.$$\n",
    "The unit vector gives the direction separately from the magnitude.\n",
    "\n",
    "### (c) External work\n",
    "For a slow move with negligible change in kinetic energy,\n",
    "$$W_{\\rm ext}=\\Delta U=q[V_f-V_i].$$\n",
    "Since $V(0,0,0)=0$ and $V(1,1,1)=-1+4=3\\,\\mathrm V$,\n",
    "$$\\boxed{W_{\\rm ext}=(2.0\\times10^{-6})(3)=+6.0\\times10^{-6}\\ \\mathrm J}.$$\n",
    "The electric field does $-6.0\\,\\mu\\mathrm J$ of work. The answer is **path independent** because this electrostatic field is the negative gradient of a smooth, single-valued potential.\n",
    "\n",
    "For a direct check, along the straight path $\\mathbf r(t)=(t,t,t)$, $\\mathbf E\\cdot\\mathbf r'(t)=4t^3-8t$, whose integral from 0 to 1 is $-3\\,\\mathrm V$. Along the three axis-aligned segments, the contributions are $0$, $-4$, and $+1\\,\\mathrm V$, again totaling $-3\\,\\mathrm V$."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f5f214e8",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-06T04:10:32.939256Z",
     "iopub.status.busy": "2026-10-06T04:10:32.935670Z",
     "iopub.status.idle": "2026-10-06T04:10:32.996308Z",
     "shell.execute_reply": "2026-10-06T04:10:32.996308Z"
    }
   },
   "outputs": [],
   "source": [
    "def potential_xyz(points):\n",
    "    \"\"\"Potential in volts for one point or an array with final axis [x,y,z].\"\"\"\n",
    "    points = np.asarray(points, dtype=float)\n",
    "    x, y, z = np.moveaxis(points, -1, 0)\n",
    "    return -x*y**2*z + 4*x*y\n",
    "\n",
    "def field_xyz(points):\n",
    "    \"\"\"Electric field -grad(V), in N/C.\"\"\"\n",
    "    points = np.asarray(points, dtype=float)\n",
    "    x, y, z = np.moveaxis(points, -1, 0)\n",
    "    return np.stack([y**2*z-4*y, 2*x*y*z-4*x, x*y**2], axis=-1)\n",
    "\n",
    "start = np.zeros(3)\n",
    "finish = np.ones(3)\n",
    "q_test = 2.0e-6                   # Positive test charge, C.\n",
    "E_point = field_xyz(finish)\n",
    "E_magnitude = np.linalg.norm(E_point)\n",
    "E_direction = E_point/E_magnitude\n",
    "W_external = q_test*(potential_xyz(finish)-potential_xyz(start))\n",
    "print(\"Field components (N/C):\", E_point)\n",
    "print(f\"Field magnitude: {E_magnitude:.6f} N/C\")\n",
    "print(\"Unit direction:\", E_direction)\n",
    "print(f\"External work: {W_external:.3e} J\")\n",
    "\n",
    "# Gaussian quadrature integrates these polynomial line integrals accurately.\n",
    "nodes, weights = np.polynomial.legendre.leggauss(8)\n",
    "t = (nodes+1)/2                   # Map integration nodes from [-1,1] to [0,1].\n",
    "weights = weights/2\n",
    "\n",
    "def field_integral_segment(p0, p1):\n",
    "    delta = np.asarray(p1, float)-np.asarray(p0, float)\n",
    "    path_points = np.asarray(p0, float) + t[:, None]*delta\n",
    "    return np.sum(weights * (field_xyz(path_points) @ delta))\n",
    "\n",
    "straight = field_integral_segment(start, finish)\n",
    "vertices = [start, [1,0,0], [1,1,0], finish]\n",
    "broken = sum(field_integral_segment(vertices[i], vertices[i+1]) for i in range(3))\n",
    "print(f\"Integral E dot dl: straight = {straight:.6f} V; segmented = {broken:.6f} V\")\n",
    "np.testing.assert_allclose([straight, broken], [-3., -3.], atol=1e-12)\n",
    "np.testing.assert_allclose(-q_test*np.array([straight, broken]), W_external)\n",
    "\n",
    "# Check the analytical gradient independently with central finite differences.\n",
    "step = 1e-5\n",
    "numeric_gradient = np.array([\n",
    "    (potential_xyz(finish+step*u)-potential_xyz(finish-step*u))/(2*step)\n",
    "    for u in np.eye(3)\n",
    "])\n",
    "np.testing.assert_allclose(E_point, -numeric_gradient, rtol=1e-8)\n",
    "np.testing.assert_allclose(E_magnitude, np.sqrt(14))\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "efdd6441",
   "metadata": {},
   "source": [
    "## Problem 3 — Conducting sphere and concentric shell\n",
    "Given $a=0.020\\,\\mathrm m$, $b=0.050\\,\\mathrm m$, $c=0.060\\,\\mathrm m$, sphere charge $Q_s=+5.0\\,\\mu\\mathrm C$, and shell net charge $Q_{\\rm sh}=-3.0\\,\\mu\\mathrm C$.\n",
    "\n",
    "### (a) Shell surface charges\n",
    "A Gaussian surface in the shell metal has $E=0$, so its enclosed charge must vanish:\n",
    "$$Q_s+Q_b=0\\quad\\Rightarrow\\quad\\boxed{Q_b=-5.0\\,\\mu\\mathrm C}.$$\n",
    "Charge conservation on the shell gives\n",
    "$$Q_b+Q_c=Q_{\\rm sh}\\quad\\Rightarrow\\quad\\boxed{Q_c=+2.0\\,\\mu\\mathrm C}.$$\n",
    "The inner solid sphere carries its $+5.0\\,\\mu\\mathrm C$ on its own surface.\n",
    "\n",
    "### (b) Field in all four regions\n",
    "For a concentric configuration, spherical symmetry makes the field radial and constant in magnitude on a spherical Gaussian surface. Thus $4\\pi r^2 E_r=Q_{\\rm enc}/\\epsilon_0$:\n",
    "$$\\boxed{E_r(r)=\\begin{cases}\n",
    "0,&0\\le r<a,\\\\\n",
    "kQ_s/r^2,&a<r<b,\\\\\n",
    "0,&b<r<c,\\\\\n",
    "k(Q_s+Q_{\\rm sh})/r^2,&r>c.\n",
    "\\end{cases}}$$\n",
    "Both nonzero regions point radially outward. The field has different one-sided limits at charged surfaces; no single value there is assigned in the code.\n",
    "\n",
    "### (c) Potential, with $V(\\infty)=0$\n",
    "Outside, $V(r)=kQ_c/r$. Both conductors are equipotentials. Define\n",
    "$$V_{\\rm shell}=\\frac{kQ_c}{c},\\qquad\n",
    "V_{\\rm sphere}=V_{\\rm shell}+kQ_s\\left(\\frac1a-\\frac1b\\right).$$\n",
    "Integrating inward and matching boundary values gives\n",
    "$$\\boxed{V(r)=\\begin{cases}\n",
    "V_{\\rm sphere},&0\\le r\\le a,\\\\\n",
    "V_{\\rm shell}+kQ_s(1/r-1/b),&a<r<b,\\\\\n",
    "V_{\\rm shell},&b\\le r\\le c,\\\\\n",
    "kQ_c/r,&r>c.\n",
    "\\end{cases}}$$\n",
    "Numerically, $\\boxed{V_{\\rm shell}\\approx3.0\\times10^5\\,\\mathrm V}$ and $\\boxed{V_{\\rm sphere}\\approx1.65\\times10^6\\,\\mathrm V}$ (about $1.6\\times10^6\\,\\mathrm V$ at two significant figures).\n",
    "\n",
    "### (d) Graphs and boundaries\n",
    "$V$ is continuous everywhere, including $a,b,c$, but its radial derivative changes abruptly. $E_r$ jumps at **all three surfaces** because each carries surface charge. For increasing-r one-sided limits, $E_r(r_0^+)-E_r(r_0^-)=\\sigma(r_0)/\\epsilon_0$. Inside each conductor the potential is flat, not necessarily zero."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a68830b8",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-06T04:10:32.996308Z",
     "iopub.status.busy": "2026-10-06T04:10:32.996308Z",
     "iopub.status.idle": "2026-10-06T04:10:33.216372Z",
     "shell.execute_reply": "2026-10-06T04:10:33.216372Z"
    }
   },
   "outputs": [],
   "source": [
    "a, b, c = 0.020, 0.050, 0.060  # Three radii, in metres.\n",
    "Q_sphere, Q_shell = 5.0e-6, -3.0e-6\n",
    "Q_inner = -Q_sphere              # Ensures zero field in the shell metal.\n",
    "Q_outer = Q_shell-Q_inner        # Preserve the shell's net charge.\n",
    "V_shell = K*Q_outer/c\n",
    "V_sphere = V_shell + K*Q_sphere*(1/a-1/b)\n",
    "\n",
    "# Use masks rather than evaluating 1/r at r=0.\n",
    "def radial_field(r):\n",
    "    r = np.asarray(r, dtype=float)\n",
    "    if np.any(r < 0):\n",
    "        raise ValueError(\"Radius must be nonnegative.\")\n",
    "    E = np.zeros_like(r)\n",
    "    gap = (r > a) & (r < b)\n",
    "    outside = r > c\n",
    "    E[gap] = K*Q_sphere/r[gap]**2\n",
    "    E[outside] = K*Q_outer/r[outside]**2\n",
    "    E[(r == a) | (r == b) | (r == c)] = np.nan  # Surface limits differ.\n",
    "    return E\n",
    "\n",
    "def radial_potential(r):\n",
    "    r = np.asarray(r, dtype=float)\n",
    "    if np.any(r < 0):\n",
    "        raise ValueError(\"Radius must be nonnegative.\")\n",
    "    V = np.full_like(r, V_shell)\n",
    "    V[r <= a] = V_sphere\n",
    "    gap = (r > a) & (r < b)\n",
    "    outside = r > c\n",
    "    V[gap] = V_shell + K*Q_sphere*(1/r[gap]-1/b)\n",
    "    V[outside] = K*Q_outer/r[outside]\n",
    "    return V\n",
    "\n",
    "print(f\"Shell inner charge = {Q_inner*1e6:+.1f} microC\")\n",
    "print(f\"Shell outer charge = {Q_outer*1e6:+.1f} microC\")\n",
    "print(f\"Shell potential = {V_shell:.6e} V\")\n",
    "print(f\"Sphere potential = {V_sphere:.6e} V\")\n",
    "\n",
    "# Draw each field region separately to avoid false connecting lines at jumps.\n",
    "fig, axes = plt.subplots(2, 1, figsize=(9, 7), sharex=True, layout=\"constrained\")\n",
    "for low, high in [(0,a), (a,b), (b,c), (c,0.10)]:\n",
    "    r = np.linspace(low+1e-9, high-1e-9, 350)\n",
    "    axes[0].plot(r*100, radial_field(r)/1e7, color=\"tab:red\", lw=2)\n",
    "r = np.unique(np.r_[np.linspace(0, 0.10, 1200), a, b, c])\n",
    "axes[1].plot(r*100, radial_potential(r)/1e6, color=\"tab:blue\", lw=2)\n",
    "for ax in axes:\n",
    "    ax.axvspan(0, a*100, color=\"0.9\")       # Shade the solid conductor.\n",
    "    ax.axvspan(b*100, c*100, color=\"0.9\")   # Shade the shell metal.\n",
    "    for radius in [a,b,c]:\n",
    "        ax.axvline(radius*100, color=\"0.5\", ls=\":\", lw=1)\n",
    "    ax.set_xlim(0,10)\n",
    "    ax.grid(alpha=0.2)\n",
    "axes[0].set(ylabel=\"E_r (10^7 N/C)\", title=\"Concentric conductors: field and potential\")\n",
    "axes[1].set(xlabel=\"Radius r (cm)\", ylabel=\"Potential (MV)\")\n",
    "axes[0].text(1, 9, \"Sphere\", ha=\"center\")\n",
    "axes[0].text(5.5, 9, \"Shell\", ha=\"center\")\n",
    "plt.show()\n",
    "\n",
    "# Verify charge conservation and potential matching at all interfaces.\n",
    "np.testing.assert_allclose(Q_inner+Q_outer, Q_shell)\n",
    "np.testing.assert_allclose(V_shell + K*Q_sphere*(1/a-1/b), V_sphere)\n",
    "np.testing.assert_allclose(V_shell + K*Q_sphere*(1/b-1/b), V_shell)\n",
    "np.testing.assert_allclose(K*Q_outer/c, V_shell)\n",
    "probe = np.array([0.01,0.03,0.055,0.08])\n",
    "step = 1e-7\n",
    "minus_dVdr = -(radial_potential(probe+step)-radial_potential(probe-step))/(2*step)\n",
    "np.testing.assert_allclose(radial_field(probe), minus_dVdr, rtol=1e-8, atol=1e-3)\n",
    "surface_charge_densities = np.array([Q_sphere,Q_inner,Q_outer])/(4*np.pi*np.array([a,b,c])**2)\n",
    "field_jumps = np.array([K*Q_sphere/a**2, -K*Q_sphere/b**2, K*Q_outer/c**2])\n",
    "np.testing.assert_allclose(field_jumps, surface_charge_densities/EPS0)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d3dc19cc",
   "metadata": {},
   "source": [
    "### (e) Move the inner sphere off-centre\n",
    "The charges and outer spherical geometry stay fixed, and the sphere does not touch the shell.\n",
    "\n",
    "| Region | What changes? |\n",
    "|---|---|\n",
    "| Inside the displaced solid metal sphere | Still $\\mathbf E=0$. The conductor's location moves. |\n",
    "| Gap | The field changes and is no longer radial about a common centre. Surface charge on the inner sphere and cavity wall becomes nonuniform. |\n",
    "| Inside shell metal | Still $\\mathbf E=0$. |\n",
    "| Outside $r>c$ | Unchanged: $\\mathbf E=k(2.0\\,\\mu\\mathrm C)\\hat{\\mathbf r}/r^2$, measured from the shell centre. |\n",
    "\n",
    "The total cavity-wall charge remains $-5.0\\,\\mu\\mathrm C$ and the outer charge remains $+2.0\\,\\mu\\mathrm C$. The exterior spherical surface is an equipotential; with no external sources and $V(\\infty)=0$, uniqueness fixes the same radial exterior solution and uniform outer charge density. The shell potential also stays the same; the inner-sphere potential generally changes.\n",
    "\n",
    "Gap lines run from the positive sphere to the negative cavity wall, meeting **both metal surfaces perpendicularly**. The field is strongest in the narrowest gap. Exterior lines point radially outward; there are no lines in either metal. Gauss's law still holds in the gap, but the lost spherical symmetry prevents determining the local field from $E(4\\pi r^2)=Q/\\epsilon_0$.\n",
    "\n",
    "The next plot is a **qualitative sketch**, not a numerical solution for the displaced geometry. An upward offset of 1 cm is chosen only for illustration; the problem supplies no numerical offset. Curves have normal endpoint tangents, and their spacing is not calibrated to field strength."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "646591e3",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-10-06T04:10:33.218413Z",
     "iopub.status.busy": "2026-10-06T04:10:33.218413Z",
     "iopub.status.idle": "2026-10-06T04:10:33.618554Z",
     "shell.execute_reply": "2026-10-06T04:10:33.618554Z"
    }
   },
   "outputs": [],
   "source": [
    "# Qualitative cross-section for part (e); lengths here are in cm.\n",
    "fig, ax = plt.subplots(figsize=(8,8), layout=\"constrained\")\n",
    "offset = np.array([0., 1.])         # Illustrative sphere-centre displacement.\n",
    "ra, rb, rc = a*100, b*100, c*100\n",
    "ax.add_patch(Circle((0,0), rc, facecolor=\"0.85\", edgecolor=\"black\", zorder=1))\n",
    "ax.add_patch(Circle((0,0), rb, facecolor=\"white\", edgecolor=\"black\", zorder=2))\n",
    "ax.add_patch(Circle(offset, ra, facecolor=\"#ffd9d5\", edgecolor=\"tab:red\", zorder=4))\n",
    "\n",
    "# Cubic Bezier curves: start along the sphere normal, end along the wall normal.\n",
    "for theta in np.linspace(0, 2*np.pi, 18, endpoint=False):\n",
    "    u = np.array([np.cos(theta), np.sin(theta)])\n",
    "    start = offset + ra*u\n",
    "    projection = np.dot(offset,u)\n",
    "    distance = -projection + np.sqrt(projection**2 + rb**2 - np.dot(offset,offset))\n",
    "    end = offset + distance*u       # Intersection with cavity wall.\n",
    "    length = np.linalg.norm(end-start)\n",
    "    cp1 = start + 0.32*length*u\n",
    "    cp2 = end - 0.32*length*(end/rb)\n",
    "    vertices = np.array([start,cp1,cp2,end])\n",
    "    curve = Path(vertices, [Path.MOVETO,Path.CURVE4,Path.CURVE4,Path.CURVE4])\n",
    "    ax.add_patch(PathPatch(curve, fill=False, color=\"tab:blue\", lw=1.2, zorder=3))\n",
    "    def bezier(t):\n",
    "        return (1-t)**3*start + 3*(1-t)**2*t*cp1 + 3*(1-t)*t**2*cp2 + t**3*end\n",
    "    ax.annotate(\"\", xy=bezier(0.58), xytext=bezier(0.48),\n",
    "                arrowprops=dict(arrowstyle=\"->\",color=\"tab:blue\"), zorder=3)\n",
    "\n",
    "# Exterior field is exactly radial and outward, independent of the offset.\n",
    "for theta in np.linspace(0, 2*np.pi, 16, endpoint=False):\n",
    "    u = np.array([np.cos(theta), np.sin(theta)])\n",
    "    ax.annotate(\"\", xy=7.4*u, xytext=rc*u,\n",
    "                arrowprops=dict(arrowstyle=\"->\",color=\"tab:orange\",lw=1.3))\n",
    "ax.text(0,1,\"+5 microC\\nE = 0\",ha=\"center\",va=\"center\",zorder=5)\n",
    "ax.text(0,-5.5,\"Shell metal: E = 0\",ha=\"center\",fontsize=9,zorder=5)\n",
    "ax.set(xlim=(-8,8),ylim=(-8,8),aspect=\"equal\",xlabel=\"x (cm)\",ylabel=\"y (cm)\",\n",
    "       title=\"Off-centre sphere: qualitative field-line sketch\")\n",
    "plt.show()\n",
    "print(\"All numerical checks passed. Off-centre gap curves are schematic.\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2cd0d68b",
   "metadata": {},
   "source": [
    "## Teaching checklist\n",
    "- An electron transfer leaves opposite plate charges; it does not put the same sign on both plates.\n",
    "- Potential decreases along the electric field. Always state the order in a potential difference.\n",
    "- External work for a slow displacement is +q delta V; field work is its negative.\n",
    "- Zero field inside a conductor implies constant potential, not zero potential.\n",
    "- The shell's net charge differs from the charge on either of its surfaces.\n",
    "- Gauss's law is always valid; extracting the local field from flux requires sufficient symmetry.\n",
    "\n",
    "**Sources:** supplied `P216_A3.pdf` (all three problems, pages 1–2) and `P216_Sol3.pdf` (worked reference solutions). Original PDFs are unchanged. Figures and code in this notebook were prepared for these solutions. Constants with extra precision account for small differences from the rounded reference answers.\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.12.14"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}