{ "cells": [ { "cell_type": "markdown", "id": "0", "metadata": {}, "source": [ "# Converting ARPES Data to Momentum-Space\n", "\n", "**Note**: We will use Fermi edge corrected data for conversion. The function `load_energy_corrected` below handles this. You can see the document on Fermi edge corrections for more details.\n", "\n", "\n", "## Converting Volumetric Data\n", "\n", "PyARPES provides a consistent interface for converting ARPES data from angle to momentum space. This means that there is only a single function that provides an entrypoint for converting volumetric data: `arpes.utilities.conversion.convert_to_kspace`.\n", "\n", "Using the PyARPES data plugins, you can be confident that your data will convert to momentum immediately after you load it, so long as it follows the PyARPES spectral model." ] }, { "cell_type": "code", "execution_count": null, "id": "1", "metadata": {}, "outputs": [], "source": [ "import matplotlib.pyplot as plt\n", "from lmfit.models import ConstantModel, QuadraticModel\n", "\n", "import arpes\n", "\n", "from arpes.fits.fit_models import AffineBroadenedFD\n", "from arpes.io import example_data\n", "from arpes.utilities.conversion import convert_to_kspace\n", "\n", "\n", "def load_energy_corrected():\n", " fmap = example_data.map.spectrum\n", " cut = fmap.sum(\"theta\", keep_attrs=True).sel(\n", " eV=slice(-0.2, 0.1),\n", " phi=slice(-0.25, 0.3),\n", " )\n", " params = AffineBroadenedFD().make_params(\n", " center=0,\n", " width=0.005,\n", " sigma=0.02,\n", " const_bkg=200000,\n", " lin_slope=0,\n", " )\n", " fit_results = cut.S.modelfit(\n", " \"eV\", AffineBroadenedFD() + ConstantModel(), params=params\n", " )\n", "\n", " edge = (\n", " fit_results.modelfit_results.F.p(\"center\")\n", " .S.modelfit(\"phi\", QuadraticModel())\n", " .modelfit_results.item()\n", " .eval(x=fmap.phi)\n", " )\n", " return fmap.G.shift_by(edge, shift_axis=\"eV\", by_axis=\"phi\")" ] }, { "cell_type": "code", "execution_count": null, "id": "2", "metadata": {}, "outputs": [], "source": [ "energy_corrected = load_energy_corrected()\n", "energy_corrected.attrs[\"energy_notation\"] = \"Binding\"" ] }, { "cell_type": "markdown", "id": "3", "metadata": {}, "source": [ "## A first conversion\n", "\n", "Now that we have our data loaded, let's convert a Fermi surface to momentum." ] }, { "cell_type": "code", "execution_count": null, "id": "4", "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "\n", "fig, ax = plt.subplots()\n", "ax = convert_to_kspace(\n", " energy_corrected.S.fat_sel(eV=0), # just convert the Fermi surface\n", " kx=np.linspace(-2.5, 1.5, 400), # along -2.5 <= kx < 1.5 (inv ang.)\n", " # with 400 steps\n", " ky=np.linspace(-2, 2, 400), # as above, with -2 <= ky < 2\n", ").S.plot(ax=ax)" ] }, { "cell_type": "markdown", "id": "5", "metadata": {}, "source": [ "## Coordinate Offsets\n", "\n", "PyARPES knows how to convert data from angle-to-momentum because data in PyARPES specifies both the experimental geometry and the angular offsets corresponding to normal emission.\n", "\n", "The geometry is specified by the data loading code which you typically do not change, but the angular offsets may change depending on how samples are mounted, DAQ software calibration, or any number of other reasons.\n", "\n", "\n", "The data sample above *already* has offsets set, which is why the $\\Gamma$ point correctly shows up at zero parallel momentum. We can also set offsets:" ] }, { "cell_type": "code", "execution_count": null, "id": "6", "metadata": {}, "outputs": [], "source": [ "# Let's look at what the assigned values were, for some reference\n", "import pprint # pretty printk\n", "\n", "pprint.pprint(energy_corrected.S.offsets)\n", "\n", "fig, ax = plt.subplots()\n", "\n", "example_fs = energy_corrected.S.fat_sel(eV=0.0).copy(deep=True)\n", "example_fs.S.apply_offsets(\n", " {\n", " \"phi\": 0.0, # controls the offset along the analyzer center axis\n", " \"theta\": 0.0,\n", " \"chi\": 0.0, # sample rotation, controls kx-ky plane orientation\n", " \"alpha\": 0.0,\n", " \"psi\": 0.0,\n", " }\n", ")\n", "ax = convert_to_kspace(\n", " example_fs,\n", " kx=np.linspace(-2.5, 1.5, 400),\n", " ky=np.linspace(-2, 2, 400),\n", ").S.plot(ax=ax)" ] }, { "cell_type": "markdown", "id": "7", "metadata": {}, "source": [ "## Best Practices for Offsets\n", "\n", "### Maintaining Offsets\n", "\n", "Once you've found offsets, they tend to be the same for all scans on a given cleave or sample. It's a good idea to keep your angular offsets in a local module or otherwise written down. I tend to put them into a local module next to notebooks so that I can run code like:\n", "\n", "```python\n", "from local.constants import angle_offsets\n", "\n", "my_scan.S.apply_offsets(angle_offsets[\"sample_1\"])\n", "```\n", "\n", "You'll likely have different ideas about what you find convenient.\n", "\n", "### Finding Offsets\n", "\n", "There are a lot of different ways to do this. You can plot like above until you are happy. Often, you might want to use a Brillouin zone overlay to help you align symmetry points." ] }, { "cell_type": "code", "execution_count": null, "id": "8", "metadata": {}, "outputs": [], "source": [ "from arpes.plotting.bz import overplot_standard\n", "import arpes.utilities.bz_spec as bz_spec\n", "from ase.lattice import HEX2D\n", "from scipy.spatial.transform import Rotation\n", "\n", "fig, ax = plt.subplots()\n", "ax = convert_to_kspace(\n", " example_fs,\n", " kx=np.linspace(-2.5, 1.5, 400),\n", " ky=np.linspace(-2, 2, 400),\n", ").S.plot(ax=ax)\n", "\n", "ws2 = HEX2D(a=bz_spec.A_WS2)\n", "r = Rotation.from_rotvec([0, 0, np.pi / 12])\n", "# plot a graphene BZ over the data... obviously silly here\n", "# to demonstrate parameters we'll rotate 30 degrees and plot several higher BZs.\n", "bz_plotter = overplot_standard(cell=ws2.tocell(), repeat=([-2, 2, 1]), transforms=[r])\n", "ax = bz_plotter(plt.gca())\n", "ax.set_xlim([-2, 2])\n", "ax.set_ylim([-2.5, 1.5])" ] }, { "cell_type": "markdown", "id": "9", "metadata": {}, "source": [ "You can also use prior knowledge of the sample setup to find good offsets, adjust from other scans, or use an interactive conversion tool:" ] }, { "cell_type": "code", "execution_count": null, "id": "10", "metadata": {}, "outputs": [], "source": [ "# from arpes.plotting.qt_ktool import ktool\n", "\n", "# ktool(example_fs, zone=\"graphene\")" ] }, { "attachments": {}, "cell_type": "markdown", "id": "11", "metadata": {}, "source": [ "![ktool.png](img/ktool.png)" ] }, { "cell_type": "markdown", "id": "12", "metadata": {}, "source": [ "## Converting the entire volume" ] }, { "cell_type": "code", "execution_count": null, "id": "13", "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "\n", "kcube = convert_to_kspace(\n", " energy_corrected,\n", " kx=np.linspace(-2.5, 1.5, 400),\n", " ky=np.linspace(-2, 2, 400),\n", ")" ] }, { "cell_type": "code", "execution_count": null, "id": "14", "metadata": {}, "outputs": [], "source": [ "fig, ax = plt.subplots()\n", "ax = kcube.sel(kx=slice(-0.02, 0.02)).mean(\"kx\").S.plot(ax=ax)" ] }, { "cell_type": "markdown", "id": "15", "metadata": {}, "source": [ "## Determining the Momentum Coordinates of Particular Points\n", "\n", "Frequently, you might need to perform some analysis in the vicinity of a particular point whose coordinates you know in angle-space, but your results need to be presented in momentum-space.\n", "\n", "Unless you want to perform your analysis in angle-space and convert results to momentum, you need to know how to project points and coordinates forward from angle to momentum.\n", "\n", "The volumetric transform is based off an interpolated *inverse* transform: the momentum coordinates are converted to angle for the interplation step, which may be contrary to your expectations if you've not worked with interpolations before. In PyARPES and most ARPES software, this inverse transform is small angle approximated in some circumstances. For this reason, PyARPES provides both an exact angle-to-momentum converter suitable for coordinates, and an exact inverse to the small angle approximated volumetric transform.\n", "\n", "The latter is typically what you want when you are doing volumetric transforms, because it tells you exactly where a feature of interest will end up in momentum after you invoke `convert_to_kspace`.\n", "\n", "Let's see how it works.\n", "\n", "First, let's pick any point in angle space which we will use to represent our feature of interest:" ] }, { "cell_type": "code", "execution_count": null, "id": "16", "metadata": {}, "outputs": [], "source": [ "ax = energy_corrected.S.fat_sel(eV=0).S.plot()\n", "plt.gca().scatter([-0.13], [-0.1], color=\"red\")\n", "\n", "# we will determine where this point goes\n", "test_point = {\n", " \"phi\": -0.13,\n", " \"theta\": -0.1,\n", " \"eV\": 0,\n", "}" ] }, { "cell_type": "code", "execution_count": null, "id": "17", "metadata": {}, "outputs": [], "source": [ "from arpes.analysis.forward_conversion import convert_coordinate_forward\n", "\n", "k_test_point = convert_coordinate_forward(energy_corrected, test_point)" ] }, { "cell_type": "code", "execution_count": null, "id": "18", "metadata": {}, "outputs": [], "source": [ "ax = kcube.sel(eV=slice(-0.05, 0.05)).sum(\"eV\").S.plot(ax=ax)\n", "plt.gca().scatter([k_test_point[\"ky\"]], [k_test_point[\"kx\"]], color=\"red\")" ] }, { "cell_type": "markdown", "id": "19", "metadata": {}, "source": [ "Excellent, this enables all kinds of analysis which we frequently want to perform.\n", "\n", "## Exactracting a Momentum Cut Passing Through Known Angular Coordinates\n", "\n", "For instance, we can determine a momentum cut passing through our point of interest." ] }, { "cell_type": "code", "execution_count": null, "id": "20", "metadata": {}, "outputs": [], "source": [ "fig, ax = plt.subplots()\n", "ky_slice = (\n", " convert_to_kspace(\n", " energy_corrected,\n", " kx=np.linspace(k_test_point[\"kx\"] - 0.02, k_test_point[\"kx\"] + 0.02, 20),\n", " ky=np.linspace(-2, 2, 800),\n", " )\n", " .mean(\"kx\")\n", " .S.plot(ax=ax)\n", ")\n", "\n", "plt.gca().scatter([k_test_point[\"ky\"]], [0.0], color=\"red\")" ] }, { "cell_type": "markdown", "id": "21", "metadata": {}, "source": [ "This is common enough that there is a utility for it in PyARPES: `convert_through_angular_point`." ] }, { "cell_type": "code", "execution_count": null, "id": "22", "metadata": {}, "outputs": [], "source": [ "from arpes.analysis.forward_conversion import convert_through_angular_point\n", "\n", "fig, ax = plt.subplots()\n", "ax = convert_through_angular_point(\n", " energy_corrected,\n", " test_point,\n", " {\"ky\": np.linspace(-1, 1, 400)}, # give the cut which has +/- 1 inv ang\n", " # in `kx` around our point of interest\n", " {\"kx\": np.linspace(-0.02, 0.02, 10)}, # take 20 milli inv ang. perpendicular\n", ").S.plot(vmax=11000) # set vmax for better comparison to above" ] }, { "cell_type": "markdown", "id": "23", "metadata": {}, "source": [ "What if we wanted to take a rotated cut through this point, rather than one aligned to the `kx` and `ky` axes above?\n", "\n", "The simplest thing to do is to apply a sample rotation offset before performing the slice, so that the momentum axes are now aligned to the desirable directions.\n", "\n", "We can use the rotation offset context manager for this." ] }, { "cell_type": "code", "execution_count": null, "id": "24", "metadata": {}, "outputs": [], "source": [ "from arpes.config import use_tex\n", "\n", "use_tex(enable=False) # just to be safe, in case you don't have LaTeX installed\n", "\n", "fig, ax = plt.subplots(3, 3, figsize=(14, 12))\n", "\n", "for inc in range(9):\n", " with energy_corrected.S.with_rotation_offset(-inc * np.pi / 8):\n", " convert_through_angular_point(\n", " energy_corrected,\n", " test_point,\n", " {\"ky\": np.linspace(-1, 1, 400)},\n", " {\"kx\": np.linspace(-0.02, 0.02, 10)},\n", " ).S.plot(vmax=11000, ax=ax.ravel()[inc])\n", " ax.ravel()[inc].set_title(f\"-{inc}pi/8\")\n", "\n", "plt.tight_layout()" ] }, { "cell_type": "markdown", "id": "25", "metadata": {}, "source": [ "\n", "Note that the first and last plots are merely reflections of one another, because a rotation by pi is equivalent to inverting the `ky` axis of the cut." ] }, { "cell_type": "markdown", "id": "26", "metadata": {}, "source": [ "## Getting an ARPES Cut Passing Through Two Angular Points\n", "\n", "Suppose we know the angular coordinates of two high symmetry points in our dataset and we wanted to get a high symmetry cut passing through both. We have just seen how to do this with one point.\n", "\n", "PyARPES has `convert_through_angular_pair` for this purpose.\n", "\n", "This type of conversion is very useful for presenting high symmetry directions in a band structure which came from a higher dimensional dataset like a map.\n", "\n", "Let's pick two points so we can be concrete." ] }, { "cell_type": "code", "execution_count": null, "id": "27", "metadata": {}, "outputs": [], "source": [ "ax = energy_corrected.S.fat_sel(eV=0).S.plot()\n", "plt.gca().scatter([0.055], [-0.013], color=\"red\")\n", "plt.gca().scatter([-0.09], [-0.18], color=\"white\")\n", "\n", "\n", "# we will get the cut through these two points\n", "p1 = {\n", " \"phi\": 0.055,\n", " \"theta\": -0.013,\n", " \"eV\": 0,\n", "}\n", "p2 = {\n", " \"phi\": -0.09,\n", " \"theta\": -0.18,\n", " \"eV\": 0,\n", "}" ] }, { "cell_type": "code", "execution_count": null, "id": "28", "metadata": {}, "outputs": [], "source": [ "from arpes.analysis.moire import angle_between_vectors\n", "\n", "kp1 = convert_coordinate_forward(energy_corrected, p1)\n", "kp2 = convert_coordinate_forward(energy_corrected, p2)\n", "\n", "\n", "def to_vec(p):\n", " return np.array([p[\"kx\"], p[\"ky\"]])\n", "\n", "\n", "print(kp1)\n", "print(kp2)\n", "print(to_vec(kp1))\n", "print(to_vec(kp2))\n", "delta = to_vec(kp2) - to_vec(kp1)\n", "ang = np.arctan2(delta[1], delta[0])\n", "print(ang)\n", "\n", "with energy_corrected.S.with_rotation_offset(-ang):\n", " kp1 = convert_coordinate_forward(energy_corrected, p1)\n", " kp2 = convert_coordinate_forward(energy_corrected, p2)\n", "\n", " print(kp1, kp2)\n", "\n", " delta = to_vec(kp2) - to_vec(kp1)\n", " print(np.arctan2(delta[1], delta[0]))" ] }, { "cell_type": "code", "execution_count": null, "id": "29", "metadata": {}, "outputs": [], "source": [ "from arpes.analysis.forward_conversion import convert_through_angular_pair\n", "\n", "fig, ax = plt.subplots()\n", "\n", "ax = convert_through_angular_pair(\n", " energy_corrected,\n", " p1,\n", " p2,\n", " {\"kx\": np.linspace(-0, 0, 400)}, # interpolate from p1 to p2 only\n", " {\"ky\": np.linspace(-0.02, 0.02, 10)}, # take 20 milli inv ang. perpendicular\n", ").S.plot(vmax=11000, ax=ax)\n", "\n", "# plotted only for legibility\n", "plt.gca().scatter([-0.007], [0.0], color=\"red\")\n", "plt.gca().scatter([1.17], [0.0], color=\"white\")" ] }, { "cell_type": "markdown", "id": "30", "metadata": {}, "source": [ "This interpolate only between `p1` and `p2`. To interpolate further in either direction, wejust need to set the margin wider. Let's interpolate one inverse angstrom past `p1` away from `p2`." ] }, { "cell_type": "code", "execution_count": null, "id": "31", "metadata": {}, "outputs": [], "source": [ "from arpes.analysis.forward_conversion import convert_through_angular_pair\n", "\n", "fig, ax = plt.subplots()\n", "\n", "ax = convert_through_angular_pair(\n", " energy_corrected,\n", " p1,\n", " p2,\n", " {\"kx\": np.linspace(-1, 0, 400)}, # interpolate 1 inv ang. further left\n", " {\"ky\": np.linspace(-0.02, 0.02, 10)}, # take 20 milli inv ang. perpendicular\n", ").S.plot(vmax=11000, ax=ax)\n", "\n", "# plotted only for legibility\n", "plt.gca().scatter([-0.007], [0.0], color=\"red\")\n", "plt.gca().scatter([1.17], [0.0], color=\"white\")" ] }, { "cell_type": "markdown", "id": "32", "metadata": {}, "source": [ "## API for Other Types of Conversions\n", "\n", "There's only one API in PyARPES for coordinate conversion. The entire geometry is specified on the data by convention. So you typically do not need to do any work to choose the appropriate conversion routine.\n", "\n", "### Extra dimensions\n", "\n", "You can convert datasets with extra dimensions, which just act as passthroughs: each slice is converted as you would expect.\n", "\n", "We can see this on our temperature dependence data." ] }, { "cell_type": "code", "execution_count": null, "id": "33", "metadata": {}, "outputs": [], "source": [ "from arpes.io import example_data\n", "from arpes.utilities.conversion import convert_to_kspace\n", "\n", "temp_dep = example_data.temperature_dependence.spectrum\n", "\n", "# We will Let PyARPES infer an appropriate kp range\n", "ktemp_dep = convert_to_kspace(temp_dep)\n", "ktemp_dep.dims" ] }, { "cell_type": "code", "execution_count": null, "id": "34", "metadata": {}, "outputs": [], "source": [ "fig, ax = plt.subplots()\n", "ax = ktemp_dep.isel(temperature=0).S.plot(ax=ax)" ] }, { "cell_type": "markdown", "id": "35", "metadata": {}, "source": [ "### Photon Energy Scans\n", "\n", "This also operates the same way. The only caveat is that you may need to assign the inner potential (on `.attrs`) in order to get good periodicity in `kz`." ] }, { "cell_type": "code", "execution_count": null, "id": "36", "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "from arpes.io import example_data\n", "from arpes.utilities.conversion import convert_to_kspace\n", "\n", "hv_scan = example_data.photon_energy.spectrum\n", "\n", "kz_data = convert_to_kspace(\n", " hv_scan.S.fat_sel(eV=0),\n", " kp=np.linspace(-2, 2, 500),\n", " kz=np.linspace(3.5, 5.2, 400),\n", ")\n", "\n", "kz_data.T.S.plot(vmax=800)\n", "plt.gca().set_title(\"\")" ] }, { "cell_type": "markdown", "id": "37", "metadata": {}, "source": [ "### Inner potential\n", "\n", "We can also set the inner potential to a different value to see its effect." ] }, { "cell_type": "code", "execution_count": null, "id": "38", "metadata": {}, "outputs": [], "source": [ "hv_fs = example_data.photon_energy.spectrum.S.fat_sel(eV=0.0)\n", "\n", "fig, axes = plt.subplots(1, 3, figsize=(16, 4))\n", "\n", "hv_fs.attrs[\"inner_potential\"] = 0.0\n", "kz_data = convert_to_kspace(\n", " hv_fs,\n", " kp=np.linspace(-2, 2, 500),\n", " kz=np.linspace(3.2, 5, 400),\n", ").T.S.plot(vmax=700, ax=axes[0])\n", "\n", "hv_fs.attrs[\"inner_potential\"] = 10.0\n", "kz_data = convert_to_kspace(\n", " hv_fs,\n", " kp=np.linspace(-2, 2, 500),\n", " kz=np.linspace(3.2 + 0.4, 5 + 0.4, 400),\n", ").T.S.plot(vmax=700, ax=axes[1])\n", "\n", "hv_fs.attrs[\"inner_potential\"] = 100.0\n", "kz_data = convert_to_kspace(\n", " hv_fs,\n", " kp=np.linspace(-2, 2, 500),\n", " kz=np.linspace(3.2 + 2.4, 5 + 2.4, 400),\n", ").T.S.plot(vmax=700, ax=axes[2])\n", "\n", "axes[0].set_title(\"Inner potential: 0 eV\")\n", "axes[1].set_title(\"Inner potential: 10 eV\")\n", "axes[2].set_title(\"Inner potential: 100 eV\")" ] }, { "cell_type": "markdown", "id": "39", "metadata": {}, "source": [ "From the above, we can see that an inner potential for this test sample of above 10eV is probably too large. 10eV is possibly too large as well, but does not look very distorted.\n", "\n", "Absolute $k_z$ values depend on the value of the inner potential $V_0$, so it's a good idea to consider also the periodicity in `kz` when setting the inner potential." ] }, { "cell_type": "markdown", "id": "40", "metadata": {}, "source": [ "## Exercises\n", "\n", "1. If you look at the PyARPES spectral model, some angles may appear to have the same effect. Under what conditions does increasing the `phi_offset` and decreasing the `theta_offset` result in the same momentum space data?\n", "2. Play around with finding good coordinate offsets and applying them.\n", "3. What is the meaning of the original `chi_offset` on `example_data.map`. What assumptions does this default behavior make?\n", "4. Use each of `convert_through_angular_point` and `convert_through_angular_pair` to make high symmetry selections through the `kx-ky` scan above." ] }, { "cell_type": "markdown", "id": "41", "metadata": {}, "source": [] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "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.12" } }, "nbformat": 4, "nbformat_minor": 5 }