Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

### Added

- One example page per object of `simgeometry.objects`, from `Line` to `Helix`
(`tutorials/objects/`, the Objects group of the Examples): how to build it, membership,
level set and a figure.
- `Helix(start, radius, pitch, turns, wire_radius, axis=..., left_handed=...)`: a helical
wire of round cross-section with hemispherical ends, e.g. the winding of an RF coil. Its
level set is the distance from the centreline (Newton steps along it from the
Expand Down
2 changes: 2 additions & 0 deletions tutorials/index.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
"9. [Boundary outlines](/simgeometry/examples/boundary-outlines/) turns 2D shapes into vertex loops and panels for boundary-element solvers, and finds where particle steps cross a boundary.\n",
"10. [Conductors and level sets](/simgeometry/examples/conductors-and-level-sets/) builds an Einzel lens from ring electrodes with voltages, reads Dirichlet values on a grid, and uses level sets for exact distances and edge crossings.\n",
"\n",
"The Objects pages below show each shape of `simgeometry.objects` on its own, from `Line` in 1D to `Helix` in 3D: how to build it, what it contains, its level set and how it looks.\n",
"\n",
"See [Building the docs](/simgeometry/development/docs/) to add an example."
]
}
Expand Down
98 changes: 98 additions & 0 deletions tutorials/objects/aperture2d.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "0",
"metadata": {},
"source": [
"# Aperture2D\n",
"\n",
"Two plates mirrored about a beam axis: a slit, or the 2D cut through a round aperture.\n",
"\n",
"`Aperture2D(axis, position, vertices)` takes the profile of one plate as `(axial, radial)`\n",
"vertices, the coordinate along the beam and the distance from the centre line at\n",
"`position`; the second plate is its mirror image. `Aperture2D.slit` builds the common\n",
"rectangular plates."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "1",
"metadata": {},
"outputs": [],
"source": [
"import matplotlib.pyplot as plt\n",
"import numpy as np\n",
"\n",
"from simgeometry import Aperture2D\n",
"from simgeometry.crossings import crossings\n",
"\n",
"slit = Aperture2D.slit(axial_position=0.3, half_width=0.1, thickness=0.05, length=0.4)\n",
"# a knife edge with a narrower bore: thin at the bore, thicker outside\n",
"knife = Aperture2D(\n",
" axis=0,\n",
" position=0.0,\n",
" vertices=[(0.8, 0.05), (0.84, 0.05), (0.95, 0.5), (0.7, 0.5)],\n",
")\n",
"print(\n",
" \"on the axis:\", slit.contains((0.3, 0.0)), \" in a plate:\", slit.contains((0.3, 0.2))\n",
")\n",
"assert not slit.contains((0.3, 0.0)) and slit.contains((0.3, -0.2))"
]
},
{
"cell_type": "markdown",
"id": "2",
"metadata": {},
"source": [
"## A beam through both apertures\n",
"\n",
"Steps along the beam axis (x) at different heights: the slit stops those with $|y| > 0.1$,\n",
"the knife edge those with $|y| > 0.05$ that got through the slit."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "3",
"metadata": {},
"outputs": [],
"source": [
"y = np.linspace(-0.425, 0.425, 18)\n",
"start = np.stack((np.zeros_like(y), y))\n",
"end = np.stack((np.ones_like(y), y))\n",
"first = crossings(slit, start, end)\n",
"end_after_slit = np.where(first.hit, first.point, end)\n",
"second = crossings(knife, start, end_after_slit)\n",
"stopped = np.where(second.hit, second.point, end_after_slit)\n",
"assert first.hit.sum() == np.sum(np.abs(y) > 0.1)\n",
"assert second.hit.sum() == np.sum((np.abs(y) > 0.05) & (np.abs(y) < 0.1))\n",
"\n",
"fig, ax = plt.subplots(figsize=(6, 3.6), layout=\"constrained\")\n",
"for shape, color in ((slit, \"tab:blue\"), (knife, \"tab:green\")):\n",
" for loop in shape.outlines():\n",
" ax.fill(*loop, color=color, alpha=0.5)\n",
"for i in range(len(y)):\n",
" ax.plot([start[0, i], stopped[0, i]], [y[i], y[i]], color=\"tab:orange\", lw=1)\n",
"ax.axhline(0.0, color=\"0.5\", lw=0.8, ls=\"--\")\n",
"ax.set(xlabel=\"x (beam axis)\", ylabel=\"y\", title=\"A slit and a knife-edge aperture\")\n",
"ax.set_aspect(\"equal\")\n",
"plt.show()"
]
}
],
"metadata": {
"docs_order": 116,
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
100 changes: 100 additions & 0 deletions tutorials/objects/aperture3d.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "0",
"metadata": {},
"source": [
"# Aperture3D\n",
"\n",
"A plate with a round opening along a coordinate axis: the extraction or acceleration grid of an\n",
"ion source, a skimmer or a beam-limiting aperture.\n",
"\n",
"`Aperture3D(axis, center_coords, vertices, lower_bound, upper_bound)` takes the opening's\n",
"radius along the axis as `(axial, radius)` vertices, interpolated linearly between them, and\n",
"the box the plate fills. It is inverted by default: the solid is the material around the\n",
"opening, and nothing outside the box. It has no level set, so crossings are sampled."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "1",
"metadata": {},
"outputs": [],
"source": [
"import matplotlib.pyplot as plt\n",
"\n",
"from simgeometry import Aperture3D\n",
"\n",
"# a 0.2 thick plate whose bore narrows from radius 0.4 to 0.2\n",
"grid_plate = Aperture3D(\n",
" axis=\"z\",\n",
" center_coords=(0.0, 0.0),\n",
" vertices=[(0.0, 0.4), (0.2, 0.2)],\n",
" lower_bound=(-1.0, -1.0, 0.0),\n",
" upper_bound=(1.0, 1.0, 0.2),\n",
")\n",
"print(\"on the axis:\", grid_plate.contains((0.0, 0.0, 0.1)))\n",
"print(\"radius 0.3 at the front:\", grid_plate.contains((0.3, 0.0, 0.01)))\n",
"print(\"radius 0.3 at the back:\", grid_plate.contains((0.3, 0.0, 0.19)))\n",
"assert not grid_plate.contains((0.3, 0.0, 0.01)) and grid_plate.contains(\n",
" (0.3, 0.0, 0.19)\n",
")"
]
},
{
"cell_type": "markdown",
"id": "2",
"metadata": {},
"source": [
"## Section and surface\n",
"\n",
"The section `y = 0` shows the tapered bore through the plate."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "3",
"metadata": {},
"outputs": [],
"source": [
"ax = grid_plate.show(y=0.0, resolution=200, display=False)\n",
"ax.set_title(\"The section y = 0 of an Aperture3D\")\n",
"plt.show()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "4",
"metadata": {},
"outputs": [],
"source": [
"import plasma_plots\n",
"import pyvista as pv\n",
"\n",
"pv.OFF_SCREEN = True # render without a window, e.g. on a docs server\n",
"\n",
"plotter = grid_plate.show_3d(\n",
" backend=\"pyvista\", display=False, resolution=96, show_bounds=False\n",
")\n",
"plasma_plots.scene(plotter, alt=\"A square plate with a tapered round hole\")"
]
}
],
"metadata": {
"docs_order": 129,
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
115 changes: 115 additions & 0 deletions tutorials/objects/circle.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
{
"cells": [
{
"cell_type": "markdown",
"id": "0",
"metadata": {},
"source": [
"# Circle\n",
"\n",
"A disc of a given radius: the cross-section of a wire, a rod or a round electrode.\n",
"\n",
"`Circle(midpoint, radius)` has an exact level set, the signed distance\n",
"`|p - midpoint| - radius`, so distances, closest points and normals are analytic."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "1",
"metadata": {},
"outputs": [],
"source": [
"import matplotlib.pyplot as plt\n",
"import numpy as np\n",
"\n",
"from simgeometry import Circle, Grid\n",
"\n",
"rod = Circle(midpoint=(0.0, 0.0), radius=0.5)\n",
"points = np.array([[0.0, 0.4, 1.0], [0.0, 0.0, 1.0]]) # (2, n): one point per column\n",
"print(\"inside:\", rod.contains_points(points))\n",
"print(\"signed distance:\", rod.signed_distance(points))\n",
"print(\"closest point to (1, 1):\", rod.closest_point((1.0, 1.0)))\n",
"print(\"normal at (0.5, 0):\", rod.surface_normal((0.5, 0.0)))\n",
"assert np.isclose(rod.signed_distance((1.0, 1.0)), np.sqrt(2) - 0.5)"
]
},
{
"cell_type": "markdown",
"id": "2",
"metadata": {},
"source": [
"## Distance field and node mask\n",
"\n",
"The level set sampled around the rod: its contours are circles, one per distance. The grid\n",
"nodes inside the rod are those `node_mask` selects."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "3",
"metadata": {},
"outputs": [],
"source": [
"x = np.linspace(-1.0, 1.0, 201)\n",
"X, Y = np.meshgrid(x, x, indexing=\"ij\")\n",
"distance = rod.signed_distance(np.stack((X.ravel(), Y.ravel()))).reshape(X.shape)\n",
"\n",
"grid = Grid(2, (-1.0, -1.0), (1.0, 1.0), (21, 21))\n",
"mask = rod.node_mask(grid).ravel()\n",
"nodes = grid.node_points()\n",
"\n",
"fig, ax = plt.subplots(figsize=(5.5, 4.5), layout=\"constrained\")\n",
"image = ax.contourf(X, Y, distance, levels=np.linspace(-0.5, 1.0, 16), cmap=\"RdBu_r\")\n",
"ax.contour(X, Y, distance, levels=[0.0], colors=\"k\")\n",
"ax.scatter(*nodes[:, mask], s=8, color=\"k\", label=\"nodes inside\")\n",
"fig.colorbar(image, ax=ax, label=\"signed distance\")\n",
"ax.set(xlabel=\"x\", ylabel=\"y\", title=\"The level set of a Circle\")\n",
"ax.set_aspect(\"equal\")\n",
"ax.legend(loc=\"upper right\")\n",
"plt.show()"
]
},
{
"cell_type": "markdown",
"id": "4",
"metadata": {},
"source": [
"## Outlines and inversion\n",
"\n",
"`outlines(max_spacing)` gives the boundary as a counter-clockwise vertex loop, for\n",
"boundary-element solvers and for plotting. An inverted circle is everything outside it: the\n",
"wall of a round chamber."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "5",
"metadata": {},
"outputs": [],
"source": [
"(loop,) = rod.outlines(max_spacing=0.05)\n",
"print(\"outline vertices:\", loop.shape[1])\n",
"\n",
"chamber = Circle(midpoint=(0.0, 0.0), radius=0.5, inverted=True)\n",
"assert np.array_equal(chamber.node_mask(grid).ravel(), ~mask)\n",
"assert np.isclose(chamber.signed_distance((1.0, 1.0)), -rod.signed_distance((1.0, 1.0)))"
]
}
],
"metadata": {
"docs_order": 111,
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 5
}
Loading
Loading