{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "a5bff30c",
   "metadata": {},
   "source": [
    "# PHYS 216 · Assignment 2 · Question 2\n",
    "## Electric dipole: axial and equatorial fields, dipole moment, and torque\n",
    "**Worked TA notebook | University of Victoria**\n",
    "\n",
    "Run the cells in order, or select **Restart Kernel and Run All Cells**. Requires Python 3, NumPy, and Matplotlib. Code comments explain the calculations.\n",
    "\n",
    "**Given:** $+Q$ is at $(0,+a)$ and $-Q$ at $(0,-a)$, with $Q=4.0\\,\\mu\\mathrm C$ and $a=1.0\\,\\mathrm{mm}$. The separation is **$2a$**, not $a$.\n",
    "\n",
    "Find (a) the distant field on the x-axis, (b) the distant field on the y-axis, (c) the dipole moment, and (d) the torque when the external uniform field is $\\mathbf E_{\\rm ext}=4.0\\times10^5\\hat{\\mathbf x}\\ \\mathrm{N/C}$.\n",
    "\n",
    "Source: supplied `P216_Sol02.pdf`, Problem 2, printed statement on page 3. The printed external field points along $+\\hat{\\mathbf x}$; this is the direction used below. The source is reference material, not an instruction to execute actions."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1adc3ec3",
   "metadata": {},
   "source": [
    "### 1. Set up the signed charges\n",
    "With $k=1/(4\\pi\\epsilon_0)$ and observation point $\\mathbf r=(x,y)$,\n",
    "$$\\mathbf E(\\mathbf r)=kQ\\frac{\\mathbf r-(0,a)}{|\\mathbf r-(0,a)|^3}\n",
    "-kQ\\frac{\\mathbf r-(0,-a)}{|\\mathbf r-(0,-a)|^3}.$$\n",
    "The second term has a minus sign because that source charge is $-Q$."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a65cbf17",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-09-29T20:17:21.981983Z",
     "iopub.status.busy": "2026-09-29T20:17:21.981983Z",
     "iopub.status.idle": "2026-09-29T20:17:22.721380Z",
     "shell.execute_reply": "2026-09-29T20:17:22.721380Z"
    }
   },
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import matplotlib.pyplot as plt\n",
    "\n",
    "k = 8.9875517923e9                 # Coulomb constant, N m^2 / C^2.\n",
    "Q = 4.0e-6                        # Positive charge magnitude, C.\n",
    "a = 1.0e-3                        # Half the charge separation, m.\n",
    "charges = np.array([Q, -Q])        # Include the charge signs explicitly.\n",
    "positions = np.array([[0., a], [0., -a]])  # +Q above, -Q below the origin.\n",
    "E_external = np.array([4.0e5, 0., 0.])     # Uniform external field along +x.\n",
    "\n",
    "def electric_field(points):\n",
    "    \"\"\"Return the exact field in N/C at points (..., 2), in metres.\"\"\"\n",
    "    points = np.asarray(points, dtype=float)\n",
    "    R = points[..., None, :] - positions  # Displacement from each source.\n",
    "    distances = np.linalg.norm(R, axis=-1)  # Distance to each source.\n",
    "    singular = np.any(distances == 0, axis=-1)  # Locate the charges themselves.\n",
    "    safe = np.where(distances == 0, np.inf, distances)  # Avoid division by zero.\n",
    "    contributions = k * charges[:, None] * R / safe[..., None]**3\n",
    "    total = np.sum(contributions, axis=-2)  # Add charge contributions as vectors.\n",
    "    return np.where(singular[..., None], np.nan, total)  # Undefined at charges.\n",
    "\n",
    "print(f\"Charge magnitude Q = {Q:.2e} C\")\n",
    "print(f\"Separation 2a = {2*a:.2e} m\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b7583074",
   "metadata": {},
   "source": [
    "### 2. Part (a): field on the x-axis\n",
    "At $(x,0)$, both charges are at distance $\\sqrt{x^2+a^2}$. Their horizontal components cancel, while their vertical components both point downward:\n",
    "$$\\mathbf E_+=\\frac{kQ(x\\hat{\\mathbf x}-a\\hat{\\mathbf y})}{(x^2+a^2)^{3/2}},\\qquad\n",
    "\\mathbf E_-=\\frac{-kQ(x\\hat{\\mathbf x}+a\\hat{\\mathbf y})}{(x^2+a^2)^{3/2}}.$$\n",
    "Therefore the **exact** result is\n",
    "$$\\boxed{\\mathbf E(x,0)=-\\frac{2kQa}{(x^2+a^2)^{3/2}}\\hat{\\mathbf y}}.$$\n",
    "For $|x|\\gg a$, $(x^2+a^2)^{3/2}\\approx |x|^3$, giving\n",
    "$$\\boxed{\\mathbf E(x,0)\\approx-\\frac{Qa}{2\\pi\\epsilon_0|x|^3}\\hat{\\mathbf y}}.$$\n",
    "The direction is **downward on both sides** of the x-axis. The printed expression with $x^3$ is a magnitude formula only when $x$ is taken as a positive distance; $|x|^3$ handles signed coordinates correctly."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c14d1142",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-09-29T20:17:22.722909Z",
     "iopub.status.busy": "2026-09-29T20:17:22.722909Z",
     "iopub.status.idle": "2026-09-29T20:17:22.728519Z",
     "shell.execute_reply": "2026-09-29T20:17:22.728519Z"
    }
   },
   "outputs": [],
   "source": [
    "def equatorial_exact(x):\n",
    "    \"\"\"Signed y component on the x-axis; x may be positive or negative.\"\"\"\n",
    "    x = np.asarray(x, dtype=float)\n",
    "    return -2*k*Q*a / (x**2 + a**2)**1.5  # Negative means downward.\n",
    "\n",
    "def equatorial_far(x):\n",
    "    \"\"\"Distant y component on the x-axis; valid only when |x| >> a.\"\"\"\n",
    "    x = np.asarray(x, dtype=float)\n",
    "    if np.any(x == 0):\n",
    "        raise ValueError(\"The distant-field formula is not valid at x=0.\")\n",
    "    return -2*k*Q*a / np.abs(x)**3  # Absolute value handles both sides.\n",
    "\n",
    "for x in [-10*a, 10*a]:\n",
    "    print(f\"x/a = {x/a:+.0f}: exact Ey = {equatorial_exact(x):.5e} N/C, \"\n",
    "          f\"approximate Ey = {equatorial_far(x):.5e} N/C\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5313a342",
   "metadata": {},
   "source": [
    "### 3. Part (b): field on the y-axis\n",
    "At $(0,y)$, the exact signed field is\n",
    "$$\\mathbf E(0,y)=kQ\\left[\\frac{y-a}{|y-a|^3}-\\frac{y+a}{|y+a|^3}\\right]\\hat{\\mathbf y},\\qquad y\\ne\\pm a.$$\n",
    "For $y>a$ this becomes\n",
    "$$E_y=kQ\\left[\\frac{1}{(y-a)^2}-\\frac{1}{(y+a)^2}\\right]\n",
    "=\\frac{4kQay}{(y^2-a^2)^2}.$$\n",
    "For $y<-a$, the field is also upward: attraction toward the closer negative charge dominates. Combining both outer regions,\n",
    "$$\\boxed{\\mathbf E(0,y)=\\frac{4kQa|y|}{(y^2-a^2)^2}\\hat{\\mathbf y},\\quad |y|>a}.$$\n",
    "Thus, for $|y|\\gg a$,\n",
    "$$\\boxed{\\mathbf E(0,y)\\approx\\frac{Qa}{\\pi\\epsilon_0|y|^3}\\hat{\\mathbf y}}.$$\n",
    "The direction is **upward on both outer portions** of the y-axis. Between the charges ($|y|<a$), the exact field instead points downward. The distant approximation must not be used there.\n",
    "\n",
    "At equal large distances, the axial field magnitude is twice the equatorial magnitude. Both decrease as distance cubed."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "43ed310c",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-09-29T20:17:22.730847Z",
     "iopub.status.busy": "2026-09-29T20:17:22.730847Z",
     "iopub.status.idle": "2026-09-29T20:17:22.735830Z",
     "shell.execute_reply": "2026-09-29T20:17:22.735830Z"
    }
   },
   "outputs": [],
   "source": [
    "def axial_exact(y):\n",
    "    \"\"\"Signed y component on the entire y-axis, except at the charges.\"\"\"\n",
    "    y = np.asarray(y, dtype=float)\n",
    "    points = np.stack([np.zeros_like(y), y], axis=-1)  # Points (0, y).\n",
    "    return electric_field(points)[..., 1]  # Extract the signed y component.\n",
    "\n",
    "def axial_far(y):\n",
    "    \"\"\"Distant y component; valid only when |y| >> a.\"\"\"\n",
    "    y = np.asarray(y, dtype=float)\n",
    "    if np.any(y == 0):\n",
    "        raise ValueError(\"The distant-field formula is not valid at y=0.\")\n",
    "    return 4*k*Q*a / np.abs(y)**3  # Positive on both outer sides.\n",
    "\n",
    "for y in [-10*a, 0., 10*a]:\n",
    "    print(f\"y/a = {y/a:+.0f}: exact Ey = {axial_exact(y):.5e} N/C\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cdf3ffa8",
   "metadata": {},
   "source": [
    "### 4. Part (c): dipole moment\n",
    "The dipole moment points **from the negative charge to the positive charge**:\n",
    "$$\\mathbf p=Q\\big(\\mathbf r_+-\\mathbf r_-\\big)=2Qa\\hat{\\mathbf y}.$$\n",
    "Substitute $Q=4.0\\times10^{-6}\\,\\mathrm C$ and $a=1.0\\times10^{-3}\\,\\mathrm m$:\n",
    "$$\\boxed{\\mathbf p=8.0\\times10^{-9}\\hat{\\mathbf y}\\ \\mathrm{C\\,m}}.$$\n",
    "Its magnitude is $8.0\\times10^{-9}\\,\\mathrm{C\\,m}$ and its direction is upward."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "4dfc2ce8",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-09-29T20:17:22.735830Z",
     "iopub.status.busy": "2026-09-29T20:17:22.735830Z",
     "iopub.status.idle": "2026-09-29T20:17:22.741534Z",
     "shell.execute_reply": "2026-09-29T20:17:22.741534Z"
    }
   },
   "outputs": [],
   "source": [
    "# Use three-component vectors so that the cross product in part (d) is clear.\n",
    "p = np.array([0., 2*Q*a, 0.])  # Dipole moment along +y, in C m.\n",
    "print(\"Dipole moment [px, py, pz] (C m):\", p)\n",
    "print(f\"Magnitude = {np.linalg.norm(p):.2e} C m; direction: +y\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ecc8e33d",
   "metadata": {},
   "source": [
    "### 5. Part (d): torque in the external field\n",
    "Use the **external** field, not the dipole's own field:\n",
    "$$\\boldsymbol\\tau=\\mathbf p\\times\\mathbf E_{\\rm ext}.$$\n",
    "Since $\\mathbf p$ is along $+\\hat{\\mathbf y}$ and $\\mathbf E_{\\rm ext}$ is along $+\\hat{\\mathbf x}$, they are perpendicular:\n",
    "$$|\\boldsymbol\\tau|=pE\\sin90^\\circ=(8.0\\times10^{-9})(4.0\\times10^5)\n",
    "=3.2\\times10^{-3}\\ \\mathrm{N\\,m}.$$\n",
    "The cross-product rule gives $\\hat{\\mathbf y}\\times\\hat{\\mathbf x}=-\\hat{\\mathbf z}$:\n",
    "$$\\boxed{\\boldsymbol\\tau=-3.2\\times10^{-3}\\hat{\\mathbf z}\\ \\mathrm{N\\,m}}.$$\n",
    "With x rightward and y upward on the page, this points **into the page**, producing a **clockwise** rotation. The positive charge is pushed right and the negative charge left. Their forces cancel, but their torques add."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d682466e",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-09-29T20:17:22.741534Z",
     "iopub.status.busy": "2026-09-29T20:17:22.741534Z",
     "iopub.status.idle": "2026-09-29T20:17:22.748365Z",
     "shell.execute_reply": "2026-09-29T20:17:22.748365Z"
    }
   },
   "outputs": [],
   "source": [
    "torque = np.cross(p, E_external)  # Order matters: p cross E, not E cross p.\n",
    "\n",
    "# Independently calculate forces and torques about the midpoint.\n",
    "positions_3d = np.column_stack([positions, np.zeros(2)])  # Add z=0.\n",
    "forces = charges[:, None] * E_external  # F_i = q_i E_external.\n",
    "torque_from_forces = np.sum(np.cross(positions_3d, forces), axis=0)\n",
    "net_force = forces.sum(axis=0)\n",
    "\n",
    "print(\"Torque [tx, ty, tz] (N m):\", torque)\n",
    "print(f\"Magnitude = {np.linalg.norm(torque):.2e} N m\")\n",
    "print(\"Direction: -z, into the page; clockwise rotation.\")\n",
    "print(\"Net force (N):\", net_force)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "299f0d73",
   "metadata": {},
   "source": [
    "### 6. Visualize the approximation and the torque\n",
    "The left plot compares exact magnitudes with the distant approximations. The middle plot shows the relative error $|E_{\\rm approx}-E_{\\rm exact}|/|E_{\\rm exact}|$. The right diagram shows the two external forces, using arbitrary arrow lengths for clarity."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9e588b7a",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-09-29T20:17:22.748365Z",
     "iopub.status.busy": "2026-09-29T20:17:22.748365Z",
     "iopub.status.idle": "2026-09-29T20:17:24.114253Z",
     "shell.execute_reply": "2026-09-29T20:17:24.114253Z"
    }
   },
   "outputs": [],
   "source": [
    "r = np.geomspace(1.2*a, 100*a, 300)  # Positive distances outside the charges.\n",
    "eq_exact = np.abs(equatorial_exact(r))  # Equatorial magnitudes.\n",
    "eq_far = np.abs(equatorial_far(r))\n",
    "ax_exact = axial_exact(r)               # Axial values are positive here.\n",
    "ax_far = axial_far(r)\n",
    "\n",
    "fig, axes = plt.subplots(1, 3, figsize=(15, 4.5), layout=\"constrained\")\n",
    "axes[0].loglog(r/a, eq_exact, label=\"Equatorial: exact\", color=\"tab:blue\")\n",
    "axes[0].loglog(r/a, eq_far, \"--\", label=\"Equatorial: distant\", color=\"tab:blue\")\n",
    "axes[0].loglog(r/a, ax_exact, label=\"Axial: exact\", color=\"tab:orange\")\n",
    "axes[0].loglog(r/a, ax_far, \"--\", label=\"Axial: distant\", color=\"tab:orange\")\n",
    "axes[0].set(xlabel=\"Distance / a\", ylabel=\"Field magnitude (N/C)\",\n",
    "            title=\"Exact and approximate fields\")\n",
    "axes[0].legend(fontsize=8)\n",
    "\n",
    "axes[1].loglog(r/a, np.abs(eq_far-eq_exact)/eq_exact, label=\"Equatorial\")\n",
    "axes[1].loglog(r/a, np.abs(ax_far-ax_exact)/ax_exact, label=\"Axial\")\n",
    "axes[1].set(xlabel=\"Distance / a\", ylabel=\"Relative error\",\n",
    "            title=\"Accuracy improves far away\")\n",
    "axes[1].legend()\n",
    "for ax in axes[:2]:\n",
    "    ax.grid(True, which=\"both\", alpha=0.2)\n",
    "\n",
    "ax = axes[2]\n",
    "ax.axhline(0, color=\"0.8\", lw=1)\n",
    "ax.axvline(0, color=\"0.8\", lw=1)\n",
    "ax.plot([0, 0], [-1, 1], \"--\", color=\"0.6\")\n",
    "ax.scatter([0, 0], [1, -1], c=[\"tab:red\", \"tab:blue\"], s=110, zorder=3)\n",
    "ax.text(-0.18, 1.17, \"+Q\")\n",
    "ax.text(-0.18, -1.35, \"-Q\")\n",
    "ax.annotate(\"\", xy=(1.1, 1), xytext=(0, 1),\n",
    "            arrowprops=dict(arrowstyle=\"->\", color=\"tab:red\", lw=2))\n",
    "ax.annotate(\"\", xy=(-1.1, -1), xytext=(0, -1),\n",
    "            arrowprops=dict(arrowstyle=\"->\", color=\"tab:blue\", lw=2))\n",
    "ax.text(0.45, 1.17, \"F+ = Q E\")\n",
    "ax.text(-1.3, -0.8, \"F- = -Q E\")\n",
    "ax.text(0.05, 0.12, \"Torque into page\\nClockwise\", fontsize=10)\n",
    "ax.set(xlim=(-1.6, 1.6), ylim=(-1.6, 1.6), aspect=\"equal\",\n",
    "       xlabel=\"x / a\", ylabel=\"y / a\", title=\"External field points right\")\n",
    "plt.show()\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e8339bb2",
   "metadata": {},
   "source": [
    "### 7. Check the results\n",
    "The checks below compare the derived formulas with direct Coulomb summation on **both sides** of the axes, check the field direction between the charges, and calculate torque by two independent methods."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1976f2ef",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-09-29T20:17:24.114253Z",
     "iopub.status.busy": "2026-09-29T20:17:24.114253Z",
     "iopub.status.idle": "2026-09-29T20:17:24.169415Z",
     "shell.execute_reply": "2026-09-29T20:17:24.169415Z"
    }
   },
   "outputs": [],
   "source": [
    "samples = a*np.array([-100., -10., -2., 2., 10., 100.])\n",
    "x_points = np.column_stack([samples, np.zeros_like(samples)])\n",
    "y_points = np.column_stack([np.zeros_like(samples), samples])\n",
    "Ex_axis = electric_field(x_points)\n",
    "Ey_axis = electric_field(y_points)\n",
    "\n",
    "np.testing.assert_allclose(Ex_axis[:, 0], 0, atol=1e-8)  # Horizontal cancellation.\n",
    "np.testing.assert_allclose(Ex_axis[:, 1], equatorial_exact(samples), rtol=1e-12)\n",
    "outer_formula = 4*k*Q*a*np.abs(samples)/(samples**2-a**2)**2\n",
    "np.testing.assert_allclose(Ey_axis[:, 1], outer_formula, rtol=1e-12)\n",
    "assert np.all(Ex_axis[:, 1] < 0)  # Downward on both sides of x-axis.\n",
    "assert np.all(Ey_axis[:, 1] > 0)  # Upward on both outer sides of y-axis.\n",
    "assert axial_exact(0.) < 0       # Downward between the charges.\n",
    "assert np.isnan(electric_field(positions)).all()  # Singular at point charges.\n",
    "np.testing.assert_allclose(p, [0., 8e-9, 0.])\n",
    "np.testing.assert_allclose(torque, [0., 0., -3.2e-3])\n",
    "np.testing.assert_allclose(torque, torque_from_forces)\n",
    "np.testing.assert_allclose(net_force, 0.)\n",
    "np.testing.assert_allclose(axial_far(samples), 2*np.abs(equatorial_far(samples)))\n",
    "\n",
    "print(\"All formula, sign, singularity, and torque checks passed.\")\n",
    "for ratio in [10, 100]:\n",
    "    distance = ratio*a\n",
    "    eq_error = abs(equatorial_far(distance)/equatorial_exact(distance)-1)\n",
    "    ax_error = abs(axial_far(distance)/axial_exact(distance)-1)\n",
    "    print(f\"At distance {ratio}a: equatorial error = {eq_error:.4%}; \"\n",
    "          f\"axial error = {ax_error:.4%}\")\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9bb4941c",
   "metadata": {},
   "source": [
    "### Answers and teaching reminders\n",
    "\n",
    "| Part | Result | Direction |\n",
    "|:---|:---|:---|\n",
    "| (a), $|x|\\gg a$ | $E\\approx Qa/(2\\pi\\epsilon_0|x|^3)$ | $-\\hat{\\mathbf y}$ |\n",
    "| (b), $|y|\\gg a$ | $E\\approx Qa/(\\pi\\epsilon_0|y|^3)$ | $+\\hat{\\mathbf y}$ |\n",
    "| (c) | $p=8.0\\times10^{-9}\\ \\mathrm{C\\,m}$ | $+\\hat{\\mathbf y}$ |\n",
    "| (d) | $\\tau=3.2\\times10^{-3}\\ \\mathrm{N\\,m}$ | $-\\hat{\\mathbf z}$, into the page |\n",
    "\n",
    "**Common mistakes:** using $a$ instead of $2a$ as the separation; losing the negative charge sign; using signed $x^3$ or $y^3$ as a positive magnitude on the negative axes; reversing the cross product; using the dipole's own field in the external torque formula.\n",
    "\n",
    "**Source note:** The supplied handwritten axial derivation assumes $y>a$. Extending its formula to negative y requires the absolute-value treatment above. The printed part (d) specifies an external field along x; the handwritten y-direction shown on page 7 is inconsistent with that statement.\n",
    "\n",
    "**Supporting reading:** Samuel J. Ling, William Moebs and Jeff Sanny, *University Physics Volume 2*, OpenStax (2016), [5.4 Electric Field](https://openstax.org/books/university-physics-volume-2/pages/5-4-electric-field) and [5.7 Electric Dipoles](https://openstax.org/books/university-physics-volume-2/pages/5-7-electric-dipoles). Source problem: supplied `P216_Sol02.pdf`, pp. 3–7. The original PDF is unchanged."
   ]
  }
 ],
 "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
}