{ "cells": [ { "cell_type": "code", "execution_count": null, "id": "c390b7f3", "metadata": { "tags": [ "remove-input" ] }, "outputs": [], "source": [ "%config InlineBackend.figure_formats = ['svg']\n", "\n", "import numpy as np\n", "from numpy.linalg import norm\n", "import matplotlib.pyplot as plt" ] }, { "cell_type": "markdown", "id": "60adc7c4", "metadata": {}, "source": [ "(analysis)=\n", "\n", "# Analysis\n", "\n", "flatspin provides several tools for analysing simulation results." ] }, { "cell_type": "markdown", "id": "99ec8b12", "metadata": {}, "source": [ "(analysis-grid)=\n", "## Working with the grid\n", "\n", "Spins in flatspin can be mapped onto a regular grid, which allows for some interesting analysis and useful functionality.\n", "The grid is what enables [spatial vector fields](fields:spatial), for example.\n", "\n", "A grid is essentially a bidirectional mapping from spin positions to grid cells.\n", "The flatspin model includes several methods for working with grids.\n", "To create a grid object (the mapping), use {func}grid() .\n", "By default, a grid is created such that a cell contains at most one spin:" ] }, { "cell_type": "code", "execution_count": null, "id": "57e7b89d", "metadata": {}, "outputs": [], "source": [ "from flatspin.model import SquareSpinIceClosed\n", "\n", "model = SquareSpinIceClosed()\n", "model.plot()\n", "\n", "def draw_grid(grid):\n", " edges = grid.edges()\n", " xmin, ymin, xmax, ymax = grid.extent\n", " plt.grid(True, c='#999', alpha=1)\n", "\n", " plt.xticks(edges[0])\n", " plt.yticks(edges[1])\n", " plt.xlim(xmin, xmax)\n", " plt.ylim(ymin, ymax)\n", "\n", "grid = model.grid()\n", "draw_grid(grid)" ] }, { "cell_type": "markdown", "id": "5dd805f7", "metadata": {}, "source": [ "The spacing between each grid can be specified with cell_size; if cell_size > lattice_spacing then a grid call may contain more than one spin:" ] }, { "cell_type": "code", "execution_count": null, "id": "1a5828c4", "metadata": {}, "outputs": [], "source": [ "grid = model.grid(cell_size=(1.5, 1.5))\n", "draw_grid(grid)\n", "model.plot();" ] }, { "cell_type": "markdown", "id": "831160c9", "metadata": {}, "source": [ "Alternatively, {func}fixed_grid()  can be used to create a grid of some fixed size:" ] }, { "cell_type": "code", "execution_count": null, "id": "5fae4596", "metadata": {}, "outputs": [], "source": [ "grid = model.fixed_grid((4,4))\n", "draw_grid(grid)\n", "model.plot();" ] }, { "cell_type": "markdown", "id": "43719f20", "metadata": {}, "source": [ "The {func}grid()  and {func}fixed_grid()  functions used above return a {class}Grid  object.\n", "\n", "A {class}Grid  can also be created manually, given a collection of points:" ] }, { "cell_type": "code", "execution_count": null, "id": "1a75e5dc", "metadata": {}, "outputs": [], "source": [ "from flatspin.grid import Grid\n", "\n", "x = np.arange(11)\n", "y = np.abs(x-5)\n", "points = np.column_stack([x, y])\n", "\n", "# When called without a cell_size, Grid attempts to auto-detect a\n", "# suitable cell_size such that each cell contains at most one point.\n", "grid = Grid(points)\n", "# For a fixed grid, use Grid.fixed_grid(points, grid_size)\n", "\n", "plt.scatter(x, y)\n", "draw_grid(grid)" ] }, { "cell_type": "markdown", "id": "055c88d2", "metadata": {}, "source": [ "### Looking up spins on the grid\n", "The {class}Grid  object enables fast lookup of the grid cell containing any spin.\n", "To map spin indices to grid cells, use {func}grid_index() .\n", "To get a list of spins inside a grid cell, use {func}point_index() ." ] }, { "cell_type": "code", "execution_count": null, "id": "6c8d6c9e", "metadata": {}, "outputs": [], "source": [ "grid = model.grid(cell_size=(1.5, 1.5))\n", "\n", "print('Spin 5 in cell:', grid.grid_index(5))\n", "print('Spin 28 in cell:', grid.grid_index(28))\n", "print('Cell (0,0) contains:', grid.point_index((0,0)))\n", "print('Cell (2,1) contains:', grid.point_index((2,1)))\n", "\n", "model.plot()\n", "# Label spin indices for reference\n", "for i in model.indices():\n", " plt.text(model.pos[i,0], model.pos[i,1], str(i), ha='center', va='center')\n", "draw_grid(grid)" ] }, { "cell_type": "markdown", "id": "2d498ef8", "metadata": {}, "source": [ "### Mapping grid values to spins\n", "A common use case is to create a grid with values, and map each value onto some property of the spins:" ] }, { "cell_type": "code", "execution_count": null, "id": "86e5bbac", "metadata": {}, "outputs": [], "source": [ "# 2D array representing grid with values\n", "# Note that the origin of the grid is in the bottom-left, hence\n", "# the first row of values map to the bottom row of the grid\n", "values = np.array(\n", " [[-1, -1, 1],\n", " [-1, 1, -1],\n", " [-1, 1, 1]])\n", "# Create an appropriate fixed grid\n", "grid = model.fixed_grid((values.shape[1], values.shape[0]))\n", "\n", "# Map all spin indices to grid index\n", "spin_inds = model.all_indices()\n", "grid_inds = grid.grid_index(spin_inds)\n", "\n", "# Set spin based on grid values\n", "model.spin[spin_inds] = values[grid_inds]\n", "\n", "draw_grid(grid)\n", "model.plot();" ] }, { "cell_type": "markdown", "id": "1df2e0cf", "metadata": {}, "source": [ "The model provides a convenient shorthand for the above code called {func}set_grid() .\n", "For example, we could spatially arrange the switching thresholds:" ] }, { "cell_type": "code", "execution_count": null, "id": "3e30b87d", "metadata": {}, "outputs": [], "source": [ "values = np.array([\n", " [0.1, 0.2, 0.3],\n", " [0.2, 0.3, 0.4],\n", " [0.3, 0.4, 0.5]\n", "])\n", "\n", "model.set_grid('threshold', values)\n", "\n", "draw_grid(grid)\n", "quiv = model.plot(C=model.threshold, cmap='coolwarm')\n", "plt.colorbar(quiv, label='threshold');" ] }, { "cell_type": "markdown", "id": "a68ca632", "metadata": {}, "source": [ "### Mapping spin values to the grid\n", "\n", "Going the other way around, we may wish to aggregate some spin value onto each grid cell." ] }, { "cell_type": "code", "execution_count": null, "id": "6b0f8b04", "metadata": {}, "outputs": [], "source": [ "# Count number of spins in each grid cell\n", "grid.add_values(np.ones(model.spin_count))" ] }, { "cell_type": "code", "execution_count": null, "id": "58f9cc1a", "metadata": {}, "outputs": [], "source": [ "# Sum the spins in each grid cell\n", "grid.add_values(model.spin)" ] }, { "cell_type": "code", "execution_count": null, "id": "2cc6996a", "metadata": {}, "outputs": [], "source": [ "# Mean spin in each cell\n", "grid.add_values(model.spin, method='mean')" ] }, { "cell_type": "code", "execution_count": null, "id": "63b58268", "metadata": {}, "outputs": [], "source": [ "# Plot total magnetization in each cell\n", "from flatspin.plotting import plot_vectors\n", "\n", "plt.subplot(121)\n", "UV = grid.add_values(model.vectors, method='sum')\n", "x, y = grid.centers()\n", "XY = np.column_stack([x, y])\n", "plot_vectors(XY, UV, normalize=True);\n", "\n", "plt.subplot(122)\n", "model.plot()\n", "draw_grid(grid)\n", "plt.tight_layout();" ] }, { "cell_type": "markdown", "id": "0c078374", "metadata": {}, "source": [ "(analysis-vertices)=\n", "## Vertices\n", "\n", "Spin ice systems are frequently analyzed in terms of its *vertices*.\n", "A *vertex* is defined by the points in the geometry where spins point either in or out.\n", "In square spin ice, a vertex is surrounded by four spins.\n", "In the plot below, the colored dots denote the vertices." ] }, { "cell_type": "code", "execution_count": null, "id": "a6fc293f", "metadata": {}, "outputs": [], "source": [ "model.randomize(seed=0x9876)\n", "model.plot()\n", "model.plot_vertices();" ] }, { "cell_type": "markdown", "id": "e82822c1", "metadata": {}, "source": [ "The spin indices of each vertex can be obtained with {func}vertices() :" ] }, { "cell_type": "code", "execution_count": null, "id": "2fcb53ff", "metadata": {}, "outputs": [], "source": [ "model.vertices()" ] }, { "cell_type": "markdown", "id": "eb54f7ee", "metadata": {}, "source": [ "Given a vertex (list of spin indices), we can obtain its position:" ] }, { "cell_type": "code", "execution_count": null, "id": "ebadbde7", "metadata": {}, "outputs": [], "source": [ "v = [5, 9, 10, 14]\n", "model.vertex_pos(v)" ] }, { "cell_type": "markdown", "id": "44467280", "metadata": {}, "source": [ "... or its magnetization:" ] }, { "cell_type": "code", "execution_count": null, "id": "d988d345", "metadata": {}, "outputs": [], "source": [ "model.vertex_mag(v)" ] }, { "cell_type": "markdown", "id": "d48e9776", "metadata": {}, "source": [ "### Vertex types\n", "The *vertex type* is defined by the dipolar energy between the spins in the vertex.\n", "In the plot above, the colors of the dots indicate the vertex type: green for type 1 (lowest energy), blue for type 2, red for type 3 and gray for type 4 (highest energy).\n", "\n", "Use {func}vertex_type()  to get the type of a vertex:" ] }, { "cell_type": "code", "execution_count": null, "id": "bb115ea3", "metadata": {}, "outputs": [], "source": [ "[model.vertex_type(v) for v in model.vertices()]" ] }, { "cell_type": "markdown", "id": "a9f2a46a", "metadata": {}, "source": [ "A measure of the degree of frustration in a spin ice system is the *vertex count*:" ] }, { "cell_type": "code", "execution_count": null, "id": "bb8ab857", "metadata": {}, "outputs": [], "source": [ "print(\"Vertex counts:\", model.vertex_count())" ] }, { "cell_type": "markdown", "id": "9d8df1d0", "metadata": {}, "source": [ "... or as fractions of the number of vertices, the *vertex population*:" ] }, { "cell_type": "code", "execution_count": null, "id": "dbd0aa2e", "metadata": {}, "outputs": [], "source": [ "print(\"Vertex population:\", model.vertex_population())" ] }, { "cell_type": "markdown", "id": "249b7708", "metadata": {}, "source": [ "### Vertex magnetization" ] }, { "cell_type": "markdown", "id": "8cc8222b", "metadata": {}, "source": [ "We may also be interested in the *vertex magnetization*:" ] }, { "cell_type": "code", "execution_count": null, "id": "9d3abcf7", "metadata": {}, "outputs": [], "source": [ "model.plot_vertex_mag();" ] }, { "cell_type": "markdown", "id": "3766a58c", "metadata": {}, "source": [ "(analysis-vertex-detection)=\n", "### Vertex detection\n", "\n", "Vertices are detected automatically using the {mod}flatspin.vertices module.\n", "You can use the module to find vertices of a geometry given a list of spin positions and angles, i.e., without having to create a [model object](model).\n", "Vertex detection relies on the [Grid](analysis-grid) to find vertex locations, using a sliding window of some size." ] }, { "cell_type": "code", "execution_count": null, "id": "64a25f8a", "metadata": {}, "outputs": [], "source": [ "from flatspin.model import KagomeSpinIce\n", "\n", "# Pretend we have no model object\n", "model2 = KagomeSpinIce(size=(3,3), init='random')\n", "pos = model2.pos\n", "angle = model2.angle\n", "spin = model2.spin\n", "mag = model2.vectors\n", "grid = Grid(pos)\n", "\n", "plot_vectors(pos, mag)\n", "# Label spin indices for reference\n", "for i in range(len(pos)):\n", " plt.text(pos[i,0], pos[i,1], str(i), ha='center', va='center')\n", "draw_grid(grid)" ] }, { "cell_type": "markdown", "id": "467619e1", "metadata": {}, "source": [ "For kagome spin ice, vertices fall inside a 3x2 grid window:" ] }, { "cell_type": "code", "execution_count": null, "id": "7832c39c", "metadata": {}, "outputs": [], "source": [ "from flatspin.vertices import find_vertices, vertex_pos, vertex_type\n", "\n", "win_size = (2, 3)\n", "vi, vj, vertices = find_vertices(grid, pos, angle, win_size)\n", "display(vertices)" ] }, { "cell_type": "code", "execution_count": null, "id": "996a4437", "metadata": {}, "outputs": [], "source": [ "vpos = vertex_pos(pos, vertices)\n", "vtype = [vertex_type(spin[v], pos[v], angle[v]) for v in vertices]\n", "print(vtype)\n", "\n", "plot_vectors(pos, mag)\n", "plt.scatter(*vpos.T, c=vtype, cmap='vertex-type');" ] }, { "cell_type": "markdown", "id": "02e76558", "metadata": {}, "source": [ "(analysis-energy)=\n", "## Energy\n", "\n", "The energy of each spin is derived from the total fields acting on each spin, i.e., the dipolar fields, external fields and thermal fields:\n", "\n", "$E_i = \\vec{h}_i \\cdot m_i$\n", "\n", "In other words, the energy of spin $i$ is the total field acting on the spin, projected onto its magnetization vector.\n", "Hence only the parallel component of the field contributes to the energy.\n", "\n", "Below we obtain the energy of each spin by calling {func}energy() .\n", "Next we use the energy to color the arrows of each spin in a relaxed pinwheel system.\n", "Notice how the spins that are part of the domain walls (the boundaries of the ferromagnetic domains) have higher energy than the spins that are well inside the domains.\n", "\n", "{note}\n", "The unit of the energy is solely the Zeeman energy, derived from the field at each spin multiplied by its magnetization.\n", "Because flatspin uses reduced magnetization units, i.e., for each spin $m_i = 1$, the energy provided by {func}energy()  also follows this reduced unit scheme (only implicitly multiplied by $m_i = 1$).\n", "To retrieve a physical energy unit, simply multiply the value by the field unit and whatever magnetization you want to ascribe to a single spin.\n", "" ] }, { "cell_type": "code", "execution_count": null, "id": "96d2704a", "metadata": {}, "outputs": [], "source": [ "from flatspin.model import PinwheelSpinIceLuckyKnot\n", "model = PinwheelSpinIceLuckyKnot(size=(25,25), alpha=0.1, init='random', use_opencl=True)\n", "\n", "print(\"Total energy (randomized):\", model.total_energy())\n", "\n", "model.relax()\n", "#model.plot_vertex_mag();\n", "#plt.figure()\n", "\n", "E = model.energy()\n", "print(\"Total energy (relaxed):\", np.sum(E))\n", "\n", "quiv = model.plot(C=E, cmap='plasma')\n", "plt.colorbar(quiv);" ] }, { "cell_type": "markdown", "id": "a55dbfc2", "metadata": {}, "source": [ "Since {func}energy()  considers all fields in the energy calculations, an external field will change the energy landscape:" ] }, { "cell_type": "code", "execution_count": null, "id": "7bffee0d", "metadata": {}, "outputs": [], "source": [ "# Saturating field\n", "model.set_h_ext([0.2, 0.2])\n", "\n", "E = model.energy()\n", "print(\"Total energy (with h_ext):\", np.sum(E))\n", "\n", "quiv = model.plot(C=E, cmap='plasma')\n", "plt.colorbar(quiv);" ] }, { "cell_type": "markdown", "id": "b323da4a", "metadata": {}, "source": [ "In the above plot, notice how the domain with the highest energy is pointing antiparallel to the external field." ] }, { "cell_type": "markdown", "id": "f618b8cf", "metadata": {}, "source": [ "### Dipolar energy\n", "\n", "If we consider dipolar fields only, we obtain the dipolar energy:\n", "\n", "$E_\\text{dip} = \\vec{h}_\\text{dip}^{(i)} \\cdot m_i$\n", "\n", "The dipolar energy is a measure of frustration in the system, and can be obtained with {func}dipolar_energy() :" ] }, { "cell_type": "code", "execution_count": null, "id": "ef88cd5e", "metadata": {}, "outputs": [], "source": [ "E_dip = model.dipolar_energy()\n", "print(\"Total dipolar energy:\", np.sum(E_dip))\n", "\n", "plt.figure()\n", "plt.title(f\"E_dip = {np.sum(E_dip):g}\")\n", "quiv = model.plot(C=E_dip, cmap='plasma')\n", "plt.colorbar(quiv);\n", "\n", "# Apply saturating field\n", "model.set_h_ext([0.2, 0.2])\n", "model.relax()\n", "\n", "E_dip = model.dipolar_energy()\n", "print(\"Total dipolar energy (after saturation):\", np.sum(E_dip))\n", "\n", "plt.figure()\n", "plt.title(f\"E_dip = {np.sum(E_dip):g}\")\n", "quiv = model.plot(C=E_dip, cmap='plasma')\n", "plt.colorbar(quiv);" ] }, { "cell_type": "markdown", "id": "da421d41", "metadata": {}, "source": [ "Similarly, there is {func}external_energy()  to calculate energy from external fields only, and {func}thermal_energy()  to calculate energy from thermal fields." ] } ], "metadata": { "jupytext": { "formats": "md:myst", "text_representation": { "extension": ".md", "format_name": "myst", "format_version": 0.13, "jupytext_version": "1.11.5" } }, "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.10.6" }, "source_map": [ 15, 23, 31, 44, 62, 66, 70, 74, 78, 84, 98, 105, 118, 123, 143, 148, 160, 166, 171, 176, 181, 195, 205, 209, 213, 215, 219, 222, 226, 228, 236, 238, 242, 244, 248, 250, 254, 258, 260, 269, 285, 289, 297, 304, 326, 341, 345, 354, 358, 368, 388 ] }, "nbformat": 4, "nbformat_minor": 5 }