{ "cells": [ { "cell_type": "markdown", "id": "solder-0", "metadata": {}, "source": [ "# Solder joints: force, standoff and alignment\n", "\n", "A ball grid array (BGA) joins a chip to a board with an array of solder joints: liquid\n", "solder wetting a round pad on each side. While the solder is molten, surface tension\n", "and gravity alone hold the chip, so the questions are mechanical: what force does a\n", "joint exert on its pad, how high does a chip of a given weight float, and does a\n", "misplaced chip pull itself back into line? Ken Brakke's\n", "[BGA tutorial](https://kenbrakke.com/evolver/bga/bga.htm) answers them with a series\n", "of datafiles, `bga-1.fe` to `bga-10.fe` and `bga-30.fe`, shipped with pySE\n", "(`pyse.examples`) together with his command files (`zforce.cmd`, ...). Units are\n", "cgs: lengths in cm, the solder's surface tension 480 erg/cm², forces in dynes.\n", "\n", "This notebook follows the series with `relax()`, `derivative()` (forces),\n", "optimizing parameters (`ev.parameters.optimize`) and `recipes.continuation`. It loads\n", "Brakke's datafiles as they are, where the [liquid bridge](liquid_bridge.ipynb) builds\n", "its surface from arrays; it assumes [Getting started](getting_started.md) and\n", "[Relaxing a surface](relaxing.md)." ] }, { "cell_type": "code", "execution_count": null, "id": "solder-1", "metadata": {}, "outputs": [], "source": [ "import warnings\n", "import numpy as np\n", "import matplotlib.pyplot as plt\n", "import pyvista as pv\n", "import pysurfaceevolver as pyse\n", "from pysurfaceevolver import recipes\n", "\n", "pv.set_jupyter_backend(\"static\")" ] }, { "cell_type": "markdown", "id": "solder-2", "metadata": {}, "source": [ "## The joint\n", "\n", "`bga-3.fe`: two coaxial pads of radius 0.1 cm, 0.12 cm apart, the solder's volume 1.3\n", "times the cylinder between them, with gravity. The pads are constraints (`z = 0` and\n", "`z = height`, `height` a parameter) and their rims are fixed vertices. Brakke's\n", "datafiles start from very few facets; `relax(levels=3)` refines three times." ] }, { "cell_type": "code", "execution_count": null, "id": "solder-3", "metadata": {}, "outputs": [], "source": [ "ev = pyse.Evolver(\"bga-3.fe\")\n", "result = ev.relax(levels=3)\n", "print(result)\n", "print(f\"{ev.counts['facets']} facets, energy {ev.total_energy:.6f} erg, \"\n", " f\"pressure {ev.body(1).pressure:.1f} dyn/cm^2\")\n", "ev.plot(off_screen=True, color=\"silver\")" ] }, { "cell_type": "markdown", "id": "solder-4", "metadata": {}, "source": [ "## The force on the upper pad\n", "\n", "The vertical force the joint exerts on the upper pad is minus the derivative of the\n", "equilibrium energy with respect to the pad's height. Brakke's tutorial (bga-4) compares\n", "seven ways to get it; the reliable ones move the whole joint with the pad (each vertex\n", "by `z/height` times the change) and correct for the volume change by the pressure, or\n", "re-converge after the move. `ev.derivative` does both:\n", "\n", "* `method=\"fixed\"` with `move=`: Brakke's preferred `zforce4`, no relaxing;\n", "* the default `method=\"newton\"`: Newton steps re-converge the moved surface (`zforce5`).\n", "\n", "`bga-4.fe` is the same joint with Brakke's variational integral for the force (the\n", "quantity `zforce7`), and his command file computes `zforce4`. All three agree:" ] }, { "cell_type": "code", "execution_count": null, "id": "solder-5", "metadata": {}, "outputs": [], "source": [ "def linear(ev):\n", " h = ev.parameters[\"height\"]\n", " return lambda x: np.c_[0*x[:, :2], x[:, 2]/h] # stretch with the pad\n", "\n", "ev = pyse.Evolver(\"bga-4.fe\")\n", "ev.command('read \"zforce.cmd\"')\n", "print(f\"{'facets':>7} {'fixed':>12} {'newton':>12} {'variational':>12} {'zforce4':>12}\")\n", "ev.relax(levels=2)\n", "for level in range(2, 6):\n", " if level > 2:\n", " ev.relax(levels=1)\n", " fixed = -ev.derivative(\"height\", method=\"fixed\", move=linear(ev))\n", " newton = -ev.derivative(\"height\", move=linear(ev))\n", " variational = ev.quantity(\"zforce7\").value\n", " zforce4 = float(ev.command(\"zforce4\").split()[1])\n", " print(f\"{ev.counts['facets']:7d} {fixed:12.6f} {newton:12.6f} {variational:12.6f} \"\n", " f\"{zforce4:12.6f}\")" ] }, { "cell_type": "markdown", "id": "solder-6", "metadata": {}, "source": [ "The methods agree to the printed digits at each refinement, but the force itself moves\n", "from about 30 to 17 dynes as the mesh is refined: on Brakke's starting meshes it is far\n", "from converged. Without `move`, `derivative()` would move only the pad's own row of\n", "vertices (its rim is fixed, so the rest of the mesh doesn't follow), which differs at\n", "first order in the edge length; give `move` whenever vertices are fixed to a moving\n", "wall.\n", "\n", "The axisymmetric version of the same joint, `bga-30.fe` (a string model of the\n", "meridian, Brakke's named quantities for area, volume and gravity), converges much faster\n", "and gives the reference the 3D joint approaches. Its pads are the lines `y = 0` and\n", "`y = height`, so the linear move is in `y`. Its start has every vertex on a pad (nothing\n", "free to move): `relax(levels=...)` refines before relaxing." ] }, { "cell_type": "code", "execution_count": null, "id": "solder-7", "metadata": {}, "outputs": [], "source": [ "axi = pyse.Evolver(\"bga-30.fe\")\n", "axi.relax(levels=2)\n", "for level in range(2, 8):\n", " if level > 2:\n", " axi.relax(levels=1)\n", " h = axi.parameters[\"height\"]\n", " force = -axi.derivative(\"height\", method=\"fixed\", move=lambda x: np.c_[0*x[:, :1], x[:, 1]/h])\n", " print(f\"{axi.counts['edges']:4d} edges force {force:.6f} dyn\")" ] }, { "cell_type": "markdown", "id": "solder-8", "metadata": {}, "source": [ "## Force against height\n", "\n", "Squeezing the joint (a lower `height`) pushes the pads apart harder; pulling it up\n", "turns the force into a pull. `recipes.continuation` steps the height, relaxing after\n", "each step; the force comes from `derivative` and the stability from the relax result.\n", "Below a height of about 0.09 cm the joint becomes unstable: it would rather bulge to one\n", "side (compare Brakke's `cbga2.fe` in [Brakke's samples: capillarity](brakke_capillarity.ipynb))." ] }, { "cell_type": "code", "execution_count": null, "id": "solder-9", "metadata": {}, "outputs": [], "source": [ "def force(ev):\n", " return -ev.derivative(\"height\", method=\"fixed\", move=linear(ev))\n", "\n", "rows = []\n", "with warnings.catch_warnings(): # unstable joints are expected here: plotted as such\n", " warnings.simplefilter(\"ignore\", pyse.UnstableEquilibriumWarning)\n", " for direction in (np.linspace(0.12, 0.05, 15), np.linspace(0.12, 0.20, 9)[1:]):\n", " ev = pyse.Evolver(\"bga-3.fe\")\n", " ev.relax(levels=3)\n", " for step in recipes.continuation(ev, \"height\", direction):\n", " rows.append((step.value, force(ev), step.result.stable))\n", "rows.sort()\n", "h, F, stable = (np.array(c) for c in zip(*rows))\n", "plt.plot(h[stable], F[stable], \"o-\", label=\"stable\")\n", "plt.plot(h[~stable], F[~stable], \"x--\", label=\"unstable\")\n", "plt.axhline(200, color=\"gray\", lw=0.8)\n", "plt.axhline(500, color=\"gray\", lw=0.8, ls=\":\")\n", "plt.xlabel(\"pad height (cm)\")\n", "plt.ylabel(\"force on the upper pad (dyn)\")\n", "plt.legend()\n", "plt.show()\n", "load_height = np.interp(200, F[::-1], h[::-1])\n", "print(f\"a load of 200 dyn is carried at height {load_height:.4f} cm\")" ] }, { "cell_type": "markdown", "id": "solder-10", "metadata": {}, "source": [ "## How high a loaded chip floats\n", "\n", "`bga-5.fe` puts a load of 200 dynes on the upper pad (an energy `pad_weight*z` on a\n", "vertex riding on it) and makes `height` an *optimizing parameter*: the minimization\n", "moves it along with the vertices, so `relax()` finds the height where the joint's push\n", "balances the load. The residual counts the force on each optimizing parameter, so\n", "`relax()` doesn't stop until it is balanced; at the end the derivative with the load\n", "included is close to zero (it vanishes as the mesh is refined, the move above and\n", "Evolver's own finite difference being different discretizations).\n", "\n", "Evolver differentiates the energy with respect to an optimizing parameter by finite\n", "differences (with `bga-5.fe`'s step `pdelta = 1e-5`), also for the Newton steps'\n", "second derivatives; `relax()` converges with its default tolerance all the same." ] }, { "cell_type": "code", "execution_count": null, "id": "solder-11", "metadata": {}, "outputs": [], "source": [ "ev = pyse.Evolver(\"bga-5.fe\")\n", "print(\"optimizing:\", sorted(ev.parameters.optimizing))\n", "result = ev.relax(levels=3)\n", "print(result)\n", "print(f\"height {ev.parameters['height']:.4f} cm (from the curve: {load_height:.4f}), \"\n", " f\"dE/dheight {ev.derivative('height', method='fixed', move=linear(ev)):.3g} dyn\")" ] }, { "cell_type": "markdown", "id": "solder-12", "metadata": {}, "source": [ "`ev.parameters.optimize(name, False)` fixes an optimizing parameter at its value (and\n", "`optimize(name)` frees any parameter): here the joint without the load's freedom, a\n", "fixed gap again.\n", "\n", "`bga-6.fe` loads the pad with 500 dynes. The curve says why that ends badly: the joint\n", "carries 500 dynes only in its unstable range, so the chip crushes the joint sideways.\n", "`relax()` reports it, and so does Brakke's own recipe (the height runs off below zero):" ] }, { "cell_type": "code", "execution_count": null, "id": "solder-13", "metadata": {}, "outputs": [], "source": [ "ev = pyse.Evolver(\"bga-6.fe\")\n", "result = ev.relax(levels=2)\n", "print(result.converged, f\"height {ev.parameters['height']:.3f}\")\n", "print(result.health.summary())" ] }, { "cell_type": "markdown", "id": "solder-14", "metadata": {}, "source": [ "## A chip that pulls itself into line\n", "\n", "`bga-7.fe` offsets the upper pad sideways (the parameter `offset`). The joint pulls it\n", "back: the sideways force, from `derivative` with the upper part of the joint sheared\n", "along (each vertex by `z/height` in x), grows linearly with the offset, a spring of\n", "about 1100 dyn/cm." ] }, { "cell_type": "code", "execution_count": null, "id": "solder-15", "metadata": {}, "outputs": [], "source": [ "ev = pyse.Evolver(\"bga-7.fe\")\n", "ev.relax(levels=3)\n", "shear = lambda x: np.c_[x[:, 2]/ev.parameters[\"height\"], 0*x[:, 1:]]\n", "for step in recipes.continuation(ev, \"offset\", [0.0, 0.01, 0.02, 0.04, 0.06, 0.08]):\n", " fx = -ev.derivative(\"offset\", method=\"fixed\", move=shear)\n", " print(f\"offset {step.value:.3f} cm sideways force {fx:8.3f} dyn \"\n", " f\"energy {step.energy:.5f}\")" ] }, { "cell_type": "markdown", "id": "solder-16", "metadata": {}, "source": [ "`bga-9.fe` frees both the offset and the height, with the load on: started 0.03 cm off,\n", "the chip slides back to the centre and settles at the loaded height." ] }, { "cell_type": "code", "execution_count": null, "id": "solder-17", "metadata": {}, "outputs": [], "source": [ "ev = pyse.Evolver(\"bga-9.fe\")\n", "print({k: ev.parameters[k] for k in sorted(ev.parameters.optimizing)})\n", "result = ev.relax(levels=3)\n", "print(result)\n", "print({k: round(ev.parameters[k], 6) for k in sorted(ev.parameters.optimizing)})" ] }, { "cell_type": "markdown", "id": "solder-18", "metadata": {}, "source": [ "`bga-10.fe` tilts the upper pad as well (a fixed tilt and 2D offset); the same tools\n", "apply. Brakke's `bga-11.fe` and `bga-12.fe` free the tilt too, and `bga-31.fe` adds\n", "contact angles to the axisymmetric model; in Evolver 2.70 they run away even with his\n", "own recipes, so they are not included.\n", "\n", "## Next\n", "\n", "The [slit droplet](slit_droplet.ipynb) builds a liquid against several walls and mirror\n", "planes from arrays, and closes it face by face for a volume mesh. More of Brakke's\n", "solder files (`column.fe`, `cbga1.fe`, `cbga2.fe`) are in\n", "[Brakke's samples: drops and capillarity](brakke_capillarity.ipynb)." ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" } }, "nbformat": 4, "nbformat_minor": 5 }