diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index abcb91df..7c480941 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -6,7 +6,7 @@ body: - type: textarea id: what-happened attributes: - label: What happened? + label: Description description: | Thanks for reporting a bug! Please describe what you were trying to get done. Tell us what happened, what went wrong. @@ -16,7 +16,7 @@ body: - type: textarea id: what-did-you-expect-to-happen attributes: - label: What did you expect to happen? + label: Expected behavior description: | A clear and concise description of what you expected to happen. validations: @@ -27,7 +27,8 @@ body: attributes: label: Minimal Complete Verifiable Example description: | - Minimal, self-contained copy-pastable example that demonstrates the issue. This will be automatically formatted into code, so no need for markdown backticks. + Minimal, self-contained copy-pastable example that demonstrates the issue. + This will be automatically formatted into code, so no need for markdown backticks. render: Python - type: checkboxes diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 4e1b0118..d2c885ad 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -2,4 +2,4 @@ blank_issues_enabled: false contact_links: - name: Ask a question or start a discussion url: https://github.com/kmnhan/erlabpy/discussions - about: Please ask and answer questions here. + about: Ask and answer questions on the discussions page! diff --git a/.github/ISSUE_TEMPLATE/feature-request.yml b/.github/ISSUE_TEMPLATE/feature-request.yml index 3fa28cfe..50d2a078 100644 --- a/.github/ISSUE_TEMPLATE/feature-request.yml +++ b/.github/ISSUE_TEMPLATE/feature-request.yml @@ -6,21 +6,22 @@ body: - type: textarea id: description attributes: - label: Is your feature request related to a problem? Please describe. + label: Description description: | - A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + Is your feature request related to a problem? + Please provide a clear and concise description of what the problem is. Ex. I'm always frustrated when [...] validations: required: true - type: textarea id: solution attributes: - label: Describe the solution you'd like + label: Possible solution description: | A clear and concise description of what you want to happen. - type: textarea id: alternatives attributes: - label: Describe alternatives you've considered + label: Alternatives description: | A clear and concise description of any alternative solutions or features you've considered. validations: diff --git a/docs/environment.yml b/docs/environment.yml index ee5796e7..7202b8d7 100644 --- a/docs/environment.yml +++ b/docs/environment.yml @@ -19,9 +19,8 @@ dependencies: - qtawesome>=1.3.1 - qtpy>=2.4.1 - scipy>=1.12.0 - - superqt>=0.6.2 - tqdm>=4.66.2 - - uncertainties>=3.0.1 + - uncertainties>=3.1.4 - varname>=0.13.0 - xarray>=2024.02.0 - hvplot diff --git a/docs/requirements.txt b/docs/requirements.txt index 0df5f11c..d734be72 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -14,9 +14,8 @@ pyqtgraph>=0.13.1 qtawesome>=1.3.1 qtpy>=2.4.1 scipy>=1.12.0 -superqt>=0.6.2 tqdm>=4.66.2 -uncertainties>=3.0.1 +uncertainties>=3.1.4 varname>=0.13.0 xarray>=2024.02.0 sphinx diff --git a/docs/source/conf.py b/docs/source/conf.py index d041a5ee..777e6dbb 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -63,6 +63,8 @@ # nitpicky = False # nitpick_ignore = [("py:class", "numpy.float64")] +highlight_language = "python3" + # -- Linkcode settings ------------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/extensions/linkcode.html diff --git a/docs/source/contributing.rst b/docs/source/contributing.rst index 60c0be3e..d41b985c 100644 --- a/docs/source/contributing.rst +++ b/docs/source/contributing.rst @@ -306,12 +306,9 @@ Code standards - Please try to add type annotations to your code. This will help with code completion and static analysis. -- Although it would be great to enforce static type checking, our code base currently - does not pass the tests. It would require a large amount of work to get it to pass, so - we are not enforcing it at the moment, and it is unclear whether the extra effort is - worth it. See `this article - `_ for some - reasons to avoid static type checking. +- We are in the process of adding type annotations to the codebase, and most of it + should pass `mypy `_ except for the io and + interactive modules. Documentation ============= diff --git a/docs/source/erlab.accessors.rst b/docs/source/erlab.accessors.rst index 29f5eebe..22aaf2eb 100644 --- a/docs/source/erlab.accessors.rst +++ b/docs/source/erlab.accessors.rst @@ -1,14 +1,4 @@ -Extensions to xarray (:mod:`erlab.accessors`) -============================================= +Accessors (:mod:`erlab.accessors`) +================================== .. automodule:: erlab.accessors - - - .. rubric:: Classes - - .. autosummary:: - - PlotAccessor - ImageToolAccessor - SelectionAccessor - MomentumAccessor diff --git a/docs/source/erlab.characterization.rst b/docs/source/erlab.characterization.rst deleted file mode 100644 index b3fc0eb6..00000000 --- a/docs/source/erlab.characterization.rst +++ /dev/null @@ -1,4 +0,0 @@ -Characterization (:mod:`erlab.characterization`) -================================================ - -.. automodule:: erlab.characterization diff --git a/docs/source/pyplots/norms.py b/docs/source/pyplots/norms.py index ec73475d..f9c1ab36 100644 --- a/docs/source/pyplots/norms.py +++ b/docs/source/pyplots/norms.py @@ -59,11 +59,11 @@ def sample_plot(norms, labels, kw0, kw1, cmap): figsize=eplt.figwh(), ) - for norm, label, k0, k1 in zip(norms, labels, kw0, kw1): + for norm, label, k0, k1 in zip(norms, labels, kw0, kw1, strict=True): axs[0].plot(x, norm(**k0, **k1)(x), label=label) bar_data = modulatedBarData(384, 256) - for i, (ax, norm, k1) in enumerate(zip(axs[1:], norms, kw1)): + for i, (ax, norm, k1) in enumerate(zip(axs[1:], norms, kw1, strict=True)): ax.plot( 0.5, 1, diff --git a/docs/source/reference.rst b/docs/source/reference.rst index 8decc1ab..eacba377 100644 --- a/docs/source/reference.rst +++ b/docs/source/reference.rst @@ -2,7 +2,7 @@ API Reference ************* -ERLabPy is organized into multiple subpackages and submodules. +ERLabPy is organized into multiple subpackages and submodules classified by their functionality. The following table lists the subpackages and submodules of ERLabPy. Subpackages =========== @@ -10,11 +10,11 @@ Subpackages ======================== ======================== Subpackage Description ======================== ======================== -`erlab.analysis` Data analysis -`erlab.io` Read & write ARPES data -`erlab.plotting` Plot -`erlab.interactive` Interactive plotting based on Qt and pyqtgraph -`erlab.characterization` Analyze sample characterization results such as XRD and transport measurements +`erlab.analysis` Routines for analyzing ARPES data. +`erlab.io` Reading and writing data. +`erlab.plotting` Functions related to static plotting with matplotlib. +`erlab.interactive` Interactive tools and widgets based on Qt and pyqtgraph +`erlab.accessors` `xarray accessors `_. You will not need to import this module directly. ======================== ======================== .. currentmodule:: erlab @@ -26,7 +26,7 @@ Subpackage Description erlab.io erlab.plotting erlab.interactive - erlab.characterization + erlab.accessors Submodules ========== @@ -35,9 +35,8 @@ Submodules Submodule Description ================== ================== `erlab.lattice` Tools for working with real and reciprocal lattices. -`erlab.constants` Physical constants and unit conversion -`erlab.accessors` `xarray accessors `_ -`erlab.parallel` Helpers for parallel processing +`erlab.constants` Physical constants and functions for unit conversion. +`erlab.parallel` Helpers for parallel processing. ================== ================== .. toctree:: @@ -45,5 +44,4 @@ Submodule Description erlab.lattice erlab.constants - erlab.accessors erlab.parallel diff --git a/docs/source/user-guide/curve-fitting.ipynb b/docs/source/user-guide/curve-fitting.ipynb index 09182765..dd9fac6c 100644 --- a/docs/source/user-guide/curve-fitting.ipynb +++ b/docs/source/user-guide/curve-fitting.ipynb @@ -17,25 +17,22 @@ "Curve fitting\n", "=============\n", "\n", - "ERLabPy provides two choices for curve fitting: `lmfit\n", - "`_ and `iminuit\n", - "`_. \n", + "Curve fitting in ERLabPy largely relies on `lmfit `_.\n", + "Along with some convenient models for common fitting tasks, ERLabPy provides a powerful\n", + "accessor that streamlines curve fitting on multidimensional xarray objects.\n", "\n", - "- `lmfit `_ provides a high-level interface to\n", - " optimization and curve fitting problems for Python. It builds on and extends many of\n", - " the optimization methods of :mod:`scipy.optimize`, and provides a common interface for\n", - " all of its supported optimization methods.\n", + "ERLabPy also provides optional integration of lmfit models with `iminuit\n", + "`_, which is a Python interface to the `Minuit\n", + "C++ library `_ developed at CERN.\n", "\n", - "- `iminuit `_ is a Python interface to the Minuit\n", - " C++ library, highly compatible with Jupyter notebooks and the SciPy ecosystem.\n", - " Although developed for high-energy physics, it is a simple and easy-to-use tool for\n", - " solving optimization problems.\n", + "In this tutorial, we will start with the basics of curve fitting using lmfit, introduce\n", + "some models that are available in ERLabPy, and demonstrate curve fitting with the\n", + ":meth:`modelfit ` accessor to\n", + "fit multidimensional xarray objects. Finally, we will show how to use `iminuit\n", + "`_ with lmfit models.\n", "\n", - "In this tutorial, we will show how to use both libraries to fit a simple function to a\n", - "set of data points.\n", - "\n", - "Basic fitting with lmfit\n", - "------------------------" + "Basic fitting with ``lmfit``\n", + "----------------------------" ] }, { @@ -320,11 +317,16 @@ "`_.\n", "\n", "Fitting with pre-defined models\n", - "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n", + "-------------------------------\n", "\n", "Creating composite models with different prefixes every time can be cumbersome, so\n", - "ERLabPy provides some pre-defined models in :mod:`erlab.analysis.fit.models`. One\n", - "example is :class:`MultiPeakModel `, which is\n", + "ERLabPy provides some pre-defined models in :mod:`erlab.analysis.fit.models`.\n", + "\n", + "\n", + "Fitting multiple peaks\n", + "~~~~~~~~~~~~~~~~~~~~~~\n", + "\n", + "One example is :class:`MultiPeakModel `, which is\n", "a composite model of multiple Gaussian or Lorentzian peaks on a linear background. By\n", "supplying keyword arguments, you can specify the number of peaks, their shapes, whether\n", "to multiply with a Fermi-Dirac distribution, and whether to convolve the result with\n", @@ -388,7 +390,7 @@ "metadata": {}, "outputs": [], "source": [ - "data = generate_data(bandshift=-0.2, count=5e+8).T\n", + "data = generate_data(bandshift=-0.2, count=5e8, seed=1).T\n", "cut = data.qsel(ky=0.3)\n", "cut.qplot(colorbar=True)" ] @@ -468,8 +470,8 @@ } }, "source": [ - "Fitting xarray objects\n", - "----------------------\n", + "Fitting ``xarray`` objects\n", + "--------------------------\n", "\n", "ERLabPy provides accessors for xarray objects that allows you to fit data with lmfit\n", "models: :meth:`xarray.DataArray.modelfit\n", @@ -738,11 +740,25 @@ " \"slope\": -0.1,\n", " },\n", ")\n", - "\n", + "result_ds" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's overlay the fitted peak positions on the data." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ "result_ds.modelfit_data.qplot()\n", - "\n", - "center_fitted = result_ds.modelfit_coefficients.sel(param=\"center\")\n", - "plt.plot(center_fitted, center_fitted.beta, \".\")" + "result_center = result_ds.sel(param=\"center\")\n", + "plt.plot(result_center.modelfit_coefficients, result_center.beta, '.-')" ] }, { @@ -834,15 +850,15 @@ " guess=True,\n", " )\n", "\n", - ".. note ::\n", - "\n", - " - Note that the initial run will take a long time due to the overhead of creating\n", - " parallel workers. Subsequent calls will run faster, since joblib's default backend\n", - " will try to reuse the workers.\n", - " \n", - " - The accessor has some intrinsic overhead due to post-processing. If you need the\n", - " best performance, handle the parallelization yourself with joblib and\n", - " :meth:`lmfit.Model.fit `.\n", + " .. note ::\n", + " \n", + " - Note that the initial run will take a long time due to the overhead of creating\n", + " parallel workers. Subsequent calls will run faster, since joblib's default backend\n", + " will try to reuse the workers.\n", + " \n", + " - The accessor has some intrinsic overhead due to post-processing. If you need the\n", + " best performance, handle the parallelization yourself with joblib and\n", + " :meth:`lmfit.Model.fit `.\n", "\n", "Saving and loading fits\n", "~~~~~~~~~~~~~~~~~~~~~~~\n", @@ -948,8 +964,16 @@ "Also check out the interactive Fermi edge fitting tool,\n", ":func:`erlab.interactive.goldtool`.\n", "\n", - "Using iminuit\n", - "-------------\n", + "Using ``iminuit``\n", + "-----------------\n", + "\n", + ".. note::\n", + "\n", + " This part requires `iminuit `_.\n", + "\n", + "`iminuit `_ is a powerful Python interface to the\n", + "`Minuit C++ library `_ developed at\n", + "CERN. To learn more, see the `iminuit documentation `_.\n", "\n", "ERLabPy provides a thin wrapper around :class:`iminuit.Minuit` that allows you to use\n", "lmfit models with iminuit. The example below conducts the same fit as the previous one,\n", @@ -1049,7 +1073,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.8" + "version": "3.11.9" } }, "nbformat": 4, diff --git a/docs/source/user-guide/indexing.ipynb b/docs/source/user-guide/indexing.ipynb index 5ab8503e..5ae0fd74 100644 --- a/docs/source/user-guide/indexing.ipynb +++ b/docs/source/user-guide/indexing.ipynb @@ -47,7 +47,7 @@ "source": [ "from erlab.io.exampledata import generate_data\n", "\n", - "dat = generate_data()" + "dat = generate_data(seed=1)" ] }, { diff --git a/docs/source/user-guide/io.ipynb b/docs/source/user-guide/io.ipynb index 6eb40d5d..3dae1c84 100644 --- a/docs/source/user-guide/io.ipynb +++ b/docs/source/user-guide/io.ipynb @@ -120,6 +120,19 @@ "Loading ARPES data\n", "------------------\n", "\n", + ".. warning::\n", + "\n", + " ERLabPy is still in development and the API may change. Some major changes regarding\n", + " data loading and handling are planned:\n", + "\n", + " - The `xarray datatree structure `_\n", + " will enable much more intuitive and powerful data handling. Once the feature gets\n", + " incorporated into xarray, ERLabPy will be updated to use it.\n", + "\n", + " - A universal translation layer between true data header attributes and\n", + " human-readable representations will be implemented. This will allow for more\n", + " consistent and user-friendly data handling.\n", + "\n", "ERLabPy's data loading framework consists of various plugins, or *loaders*, each\n", "designed to load data from a different beamline or laboratory. Each loader is a class\n", "that has a ``load`` method which takes a file path or sequence number and returns data.\n", @@ -525,6 +538,7 @@ " temp=temp,\n", " bandshift=bandshift,\n", " assign_attributes=False,\n", + " seed=1,\n", " ).T\n", "\n", " # Rename coordinates. The loader must rename them back to the original names.\n", @@ -1070,7 +1084,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.8" + "version": "3.11.9" } }, "nbformat": 4, diff --git a/docs/source/user-guide/kconv.ipynb b/docs/source/user-guide/kconv.ipynb index e5cda9d6..aebc5199 100644 --- a/docs/source/user-guide/kconv.ipynb +++ b/docs/source/user-guide/kconv.ipynb @@ -40,7 +40,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": { "editable": true, "slideshow": { @@ -56,7 +56,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": { "nbsphinx": "hidden" }, @@ -94,7 +94,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": { "editable": true, "slideshow": { @@ -102,499 +102,11 @@ }, "tags": [] }, - "outputs": [ - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "
<xarray.DataArray (eV: 500, beta: 60, alpha: 500)>\n",
-       "2.358 3.405 2.24 1.645 0.6441 ... 0.0004334 8.253e-07 6.374e-09 6.121e-12\n",
-       "Coordinates:\n",
-       "  * alpha    (alpha) float64 -15.0 -14.94 -14.88 -14.82 ... 14.88 14.94 15.0\n",
-       "  * beta     (beta) float64 -15.0 -14.49 -13.98 -13.47 ... 13.98 14.49 15.0\n",
-       "  * eV       (eV) float64 -0.45 -0.4489 -0.4477 -0.4466 ... 0.1177 0.1189 0.12\n",
-       "    xi       float64 0.0\n",
-       "    delta    float64 0.0\n",
-       "    hv       float64 50.0\n",
-       "Attributes:\n",
-       "    configuration:        1\n",
-       "    temp_sample:          20.0\n",
-       "    sample_workfunction:  4.5
" - ], - "text/plain": [ - "\n", - "2.358 3.405 2.24 1.645 0.6441 ... 0.0004334 8.253e-07 6.374e-09 6.121e-12\n", - "Coordinates:\n", - " * alpha (alpha) float64 -15.0 -14.94 -14.88 -14.82 ... 14.88 14.94 15.0\n", - " * beta (beta) float64 -15.0 -14.49 -13.98 -13.47 ... 13.98 14.49 15.0\n", - " * eV (eV) float64 -0.45 -0.4489 -0.4477 -0.4466 ... 0.1177 0.1189 0.12\n", - " xi float64 0.0\n", - " delta float64 0.0\n", - " hv float64 50.0\n", - "Attributes:\n", - " configuration: 1\n", - " temp_sample: 20.0\n", - " sample_workfunction: 4.5" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "from erlab.io.exampledata import generate_data_angles\n", "\n", - "dat = generate_data_angles(assign_attributes=True, seed=1).T\n", + "dat = generate_data_angles(shape=(200, 60, 300), assign_attributes=True, seed=1).T\n", "dat" ] }, @@ -607,672 +119,9 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "application/pdf": "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MSA0NjAgODM4IDYzNgo5NTAgNzgwIDI3NSAzOTAgMzkwIDUwMCA4MzggMzE4IDM2MSAzMTggMzM3IDYzNiA2MzYgNjM2IDYzNiA2MzYgNjM2IDYzNiA2MzYKNjM2IDYzNiAzMzcgMzM3IDgzOCA4MzggODM4IDUzMSAxMDAwIDY4NCA2ODYgNjk4IDc3MCA2MzIgNTc1IDc3NSA3NTIgMjk1CjI5NSA2NTYgNTU3IDg2MyA3NDggNzg3IDYwMyA3ODcgNjk1IDYzNSA2MTEgNzMyIDY4NCA5ODkgNjg1IDYxMSA2ODUgMzkwIDMzNwozOTAgODM4IDUwMCA1MDAgNjEzIDYzNSA1NTAgNjM1IDYxNSAzNTIgNjM1IDYzNCAyNzggMjc4IDU3OSAyNzggOTc0IDYzNCA2MTIKNjM1IDYzNSA0MTEgNTIxIDM5MiA2MzQgNTkyIDgxOCA1OTIgNTkyIDUyNSA2MzYgMzM3IDYzNiA4MzggNjAwIDYzNiA2MDAgMzE4CjM1MiA1MTggMTAwMCA1MDAgNTAwIDUwMCAxMzUwIDYzNSA0MDAgMTA3MCA2MDAgNjg1IDYwMCA2MDAgMzE4IDMxOCA1MTggNTE4CjU5MCA1MDAgMTAwMCA1MDAgMTAwMCA1MjEgNDAwIDEwMjggNjAwIDUyNSA2MTEgMzE4IDQwMSA2MzYgNjM2IDYzNiA2MzYgMzM3CjUwMCA1MDAgMTAwMCA0NzEgNjE3IDgzOCAzNjEgMTAwMCA1MDAgNTAwIDgzOCA0MDEgNDAxIDUwMCA2MzYgNjM2IDMxOCA1MDAKNDAxIDQ3MSA2MTcgOTY5IDk2OSA5NjkgNTMxIDY4NCA2ODQgNjg0IDY4NCA2ODQgNjg0IDk3NCA2OTggNjMyIDYzMiA2MzIgNjMyCjI5NSAyOTUgMjk1IDI5NSA3NzUgNzQ4IDc4NyA3ODcgNzg3IDc4NyA3ODcgODM4IDc4NyA3MzIgNzMyIDczMiA3MzIgNjExIDYwOAo2MzAgNjEzIDYxMyA2MTMgNjEzIDYxMyA2MTMgOTk1IDU1MCA2MTUgNjE1IDYxNSA2MTUgMjc4IDI3OCAyNzggMjc4IDYxMiA2MzQKNjEyIDYxMiA2MTIgNjEyIDYxMiA4MzggNjEyIDYzNCA2MzQgNjM0IDYzNCA1OTIgNjM1IDU5MiBd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MDEgNDYwIDgzOCA2MzYKOTUwIDc4MCAyNzUgMzkwIDM5MCA1MDAgODM4IDMxOCAzNjEgMzE4IDMzNyA2MzYgNjM2IDYzNiA2MzYgNjM2IDYzNiA2MzYgNjM2CjYzNiA2MzYgMzM3IDMzNyA4MzggODM4IDgzOCA1MzEgMTAwMCA2ODQgNjg2IDY5OCA3NzAgNjMyIDU3NSA3NzUgNzUyIDI5NQoyOTUgNjU2IDU1NyA4NjMgNzQ4IDc4NyA2MDMgNzg3IDY5NSA2MzUgNjExIDczMiA2ODQgOTg5IDY4NSA2MTEgNjg1IDM5MCAzMzcKMzkwIDgzOCA1MDAgNTAwIDYxMyA2MzUgNTUwIDYzNSA2MTUgMzUyIDYzNSA2MzQgMjc4IDI3OCA1NzkgMjc4IDk3NCA2MzQgNjEyCjYzNSA2MzUgNDExIDUyMSAzOTIgNjM0IDU5MiA4MTggNTkyIDU5MiA1MjUgNjM2IDMzNyA2MzYgODM4IDYwMCA2MzYgNjAwIDMxOAozNTIgNTE4IDEwMDAgNTAwIDUwMCA1MDAgMTM0MiA2MzUgNDAwIDEwNzAgNjAwIDY4NSA2MDAgNjAwIDMxOCAzMTggNTE4IDUxOAo1OTAgNTAwIDEwMDAgNTAwIDEwMDAgNTIxIDQwMCAxMDIzIDYwMCA1MjUgNjExIDMxOCA0MDEgNjM2IDYzNiA2MzYgNjM2IDMzNwo1MDAgNTAwIDEwMDAgNDcxIDYxMiA4MzggMzYxIDEwMDAgNTAwIDUwMCA4MzggNDAxIDQwMSA1MDAgNjM2IDYzNiAzMTggNTAwCjQwMSA0NzEgNjEyIDk2OSA5NjkgOTY5IDUzMSA2ODQgNjg0IDY4NCA2ODQgNjg0IDY4NCA5NzQgNjk4IDYzMiA2MzIgNjMyIDYzMgoyOTUgMjk1IDI5NSAyOTUgNzc1IDc0OCA3ODcgNzg3IDc4NyA3ODcgNzg3IDgzOCA3ODcgNzMyIDczMiA3MzIgNzMyIDYxMSA2MDUKNjMwIDYxMyA2MTMgNjEzIDYxMyA2MTMgNjEzIDk4MiA1NTAgNjE1IDYxNSA2MTUgNjE1IDI3OCAyNzggMjc4IDI3OCA2MTIgNjM0CjYxMiA2MTIgNjEyIDYxMiA2MTIgODM4IDYxMiA2MzQgNjM0IDYzNCA2MzQgNTkyIDYzNSA1OTIg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L0NyZWF0aW9uRGF0ZSAoRDoyMDI0MDQyMjIxNTY1MiswOScwMCcpID4+CmVuZG9iagp4cmVmCjAgNDIKMDAwMDAwMDAwMCA2NTUzNSBmIAowMDAwMDAwMDE2IDAwMDAwIG4gCjAwMDAwNTIwMzMgMDAwMDAgbiAKMDAwMDAwOTIxNCAwMDAwMCBuIAowMDAwMDA5MjU3IDAwMDAwIG4gCjAwMDAwMDkzNTYgMDAwMDAgbiAKMDAwMDAwOTM3NyAwMDAwMCBuIAowMDAwMDA5Mzk4IDAwMDAwIG4gCjAwMDAwMDAwNjUgMDAwMDAgbiAKMDAwMDAwMDM0NCAwMDAwMCBuIAowMDAwMDAxMjYwIDAwMDAwIG4gCjAwMDAwMDAyMDggMDAwMDAgbiAKMDAwMDAwMTI0MCAwMDAwMCBuIAowMDAwMDA5NDk0IDAwMDAwIG4gCjAwMDAwMDI1NTUgMDAwMDAgbiAKMDAwMDAwMjM0MCAwMDAwMCBuIAowMDAwMDAyMDA4IDAwMDAwIG4gCjAwMDAwMDM2MDggMDAwMDAgbiAKMDAwMDAwMTI4MCAwMDAwMCBuIAowMDAwMDAxNDQyIDAwMDAwIG4gCjAwMDAwMDE1OTcgMDAwMDAgbiAKMDAwMDAwNzk1NiAwMDAwMCBuIAowMDAwMDA3NzQ5IDAwMDAwIG4gCjAwMDAwMDczMzQgMDAwMDAgbiAKMDAwMDAwOTAwOSAwMDAwMCBuIAowMDAwMDAzNjUwIDAwMDAwIG4gCjAwMDAwMDM3OTQgMDAwMDAgbiAKMDAwMDAwNDA5OCAwMDAwMCBuIAowMDAwMDA0NDIwIDAwMDAwIG4gCjAwMDAwMDQ3NDIgMDAwMDAgbiAKMDAwMDAwNDkwOCAwMDAwMCBuIAowMDAwMDA1MzIyIDAwMDAwIG4gCjAwMDAwMDU0OTQgMDAwMDAgbiAKMDAwMDAwNTY0OSAwMDAwMCBuIAowMDAwMDA1ODcyIDAwMDAwIG4gCjAwMDAwMDYwOTYgMDAwMDAgbiAKMDAwMDAwNjIxOSAwMDAwMCBuIAowMDAwMDA2MzA5IDAwMDAwIG4gCjAwMDAwMDY3MjIgMDAwMDAgbiAKMDAwMDAwNzA0NiAwMDAwMCBuIAowMDAwMDUyMDExIDAwMDAwIG4gCjAwMDAwNTIwOTMgMDAwMDAgbiAKdHJhaWxlcgo8PCAvU2l6ZSA0MiAvUm9vdCAxIDAgUiAvSW5mbyA0MSAwIFIgPj4Kc3RhcnR4cmVmCjUyMjUwCiUlRU9GCg==", - "image/svg+xml": [ - "\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " 2024-04-22T21:56:51.926236\n", - " image/svg+xml\n", - " \n", - " \n", - " Matplotlib v3.8.4, https://matplotlib.org/\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "\n" - ], - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "cut = dat.sel(beta=10.0, method=\"nearest\")\n", "eplt.plot_array(cut)" @@ -1302,493 +151,9 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Estimating bounds and resolution\n", - "Calculating destination coordinates\n", - "Converting ('eV', 'alpha', 'beta') -> ('eV', 'kx', 'ky')\n", - "Interpolated in 1.110 s\n" - ] - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "
<xarray.DataArray (eV: 500, kx: 310, ky: 310)>\n",
-       "nan nan nan nan nan nan nan nan nan ... 0.01834 0.02405 nan nan nan nan nan nan\n",
-       "Coordinates:\n",
-       "    xi       float64 0.0\n",
-       "    delta    float64 0.0\n",
-       "    hv       float64 50.0\n",
-       "  * eV       (eV) float64 -0.45 -0.4489 -0.4477 -0.4466 ... 0.1177 0.1189 0.12\n",
-       "  * kx       (kx) float64 -0.8956 -0.8898 -0.884 -0.8782 ... 0.884 0.8898 0.8956\n",
-       "  * ky       (ky) float64 -0.8956 -0.8898 -0.884 -0.8782 ... 0.884 0.8898 0.8956\n",
-       "Attributes:\n",
-       "    configuration:        1\n",
-       "    temp_sample:          20.0\n",
-       "    sample_workfunction:  4.5\n",
-       "    delta_offset:         0.0\n",
-       "    xi_offset:            0.0\n",
-       "    beta_offset:          0.0
" - ], - "text/plain": [ - "\n", - "nan nan nan nan nan nan nan nan nan ... 0.01834 0.02405 nan nan nan nan nan nan\n", - "Coordinates:\n", - " xi float64 0.0\n", - " delta float64 0.0\n", - " hv float64 50.0\n", - " * eV (eV) float64 -0.45 -0.4489 -0.4477 -0.4466 ... 0.1177 0.1189 0.12\n", - " * kx (kx) float64 -0.8956 -0.8898 -0.884 -0.8782 ... 0.884 0.8898 0.8956\n", - " * ky (ky) float64 -0.8956 -0.8898 -0.884 -0.8782 ... 0.884 0.8898 0.8956\n", - "Attributes:\n", - " configuration: 1\n", - " temp_sample: 20.0\n", - " sample_workfunction: 4.5\n", - " delta_offset: 0.0\n", - " xi_offset: 0.0\n", - " beta_offset: 0.0" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "dat_kconv = dat.kspace.convert()\n", "dat_kconv" @@ -1810,867 +175,9 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "application/pdf": "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NjAwIDYwMCA2MDAgNjAwIDYwMCA2MDAgNjAwIDYwMCA2MDAKNjAwIDYwMCA2MDAgNjAwIDYwMCA2MDAgNjAwIDYwMCA2MDAgNjAwIDYwMCA2MDAgNjAwIDYwMCAzMTggNDAxIDQ2MCA4MzggNjM2Cjk1MCA3ODAgMjc1IDM5MCAzOTAgNTAwIDgzOCAzMTggMzYxIDMxOCAzMzcgNjM2IDYzNiA2MzYgNjM2IDYzNiA2MzYgNjM2IDYzNgo2MzYgNjM2IDMzNyAzMzcgODM4IDgzOCA4MzggNTMxIDEwMDAgNjg0IDY4NiA2OTggNzcwIDYzMiA1NzUgNzc1IDc1MiAyOTUKMjk1IDY1NiA1NTcgODYzIDc0OCA3ODcgNjAzIDc4NyA2OTUgNjM1IDYxMSA3MzIgNjg0IDk4OSA2ODUgNjExIDY4NSAzOTAgMzM3CjM5MCA4MzggNTAwIDUwMCA2MTMgNjM1IDU1MCA2MzUgNjE1IDM1MiA2MzUgNjM0IDI3OCAyNzggNTc5IDI3OCA5NzQgNjM0IDYxMgo2MzUgNjM1IDQxMSA1MjEgMzkyIDYzNCA1OTIgODE4IDU5MiA1OTIgNTI1IDYzNiAzMzcgNjM2IDgzOCA2MDAgNjM2IDYwMCAzMTgKMzUyIDUxOCAxMDAwIDUwMCA1MDAgNTAwIDEzNTAgNjM1IDQwMCAxMDcwIDYwMCA2ODUgNjAwIDYwMCAzMTggMzE4IDUxOCA1MTgKNTkwIDUwMCAxMDAwIDUwMCAxMDAwIDUyMSA0MDAgMTAyOCA2MDAgNTI1IDYxMSAzMTggNDAxIDYzNiA2MzYgNjM2IDYzNiAzMzcKNTAwIDUwMCAxMDAwIDQ3MSA2MTcgODM4IDM2MSAxMDAwIDUwMCA1MDAgODM4IDQwMSA0MDEgNTAwIDYzNiA2MzYgMzE4IDUwMAo0MDEgNDcxIDYxNyA5NjkgOTY5IDk2OSA1MzEgNjg0IDY4NCA2ODQgNjg0IDY4NCA2ODQgOTc0IDY5OCA2MzIgNjMyIDYzMiA2MzIKMjk1IDI5NSAyOTUgMjk1IDc3NSA3NDggNzg3IDc4NyA3ODcgNzg3IDc4NyA4MzggNzg3IDczMiA3MzIgNzMyIDczMiA2MTEgNjA4CjYzMCA2MTMgNjEzIDYxMyA2MTMgNjEzIDYxMyA5OTUgNTUwIDYxNSA2MTUgNjE1IDYxNSAyNzggMjc4IDI3OCAyNzggNjEyIDYzNAo2MTIgNjEyIDYxMiA2MTIgNjEyIDgzOCA2MTIgNjM0IDYzNCA2MzQgNjM0IDU5MiA2MzUgNTkyIF0KZW5kb2JqCjE4IDAgb2JqCjw8IC9rIDIxIDAgUiAveCAyMiAwIFIgL3kgMjMgMCBSID4+CmVuZG9iagoyOCAwIG9iago8PCAvTGVuZ3RoIDI3MCAvRmlsdGVyIC9GbGF0ZURlY29kZSA+PgpzdHJlYW0KeJw1UUmSAyEMu/MKPwEvYPOeTE3NIfn/dSTTOSRS4wVJ7AqZUvjt3HKs5EdH6JY8IZ9mpSExS8pNHKcVW17DC2N7im8DlnhcRMX0nkwHqlgdzCQqlgtbXAzV0tXIe1AhS1TQkWv1TIZyG7akpXBr2ux7iKjsy6ikO7DpzlAtt7QPbP36eY2/wfkNJ59h80DBkfeIXA/7Vt/opKZd3RlgBz3QngpHBnRqMAVD1XhjzovXUSfGXqYBZ0ig4IopuBy49xkdOO8kwlFWs9DZHeH7mYlOGU7wSNxKvPc0S54ghTUl8CbpnQK+qDZ8tXqmQD+vwaee8LigpHGmaCWYQsHD1LpW/f/Ngrn8/gNa1WrCCmVuZHN0cmVhbQplbmRvYmoKMjkgMCBvYmoKPDwgL0xlbmd0aCAyMzEgL0ZpbHRlciAvRmxhdGVEZWNvZGUgPj4Kc3RyZWFtCnicNU85kgQhDMt5hT4wVRjbQL+np7Y22Pl/upKZTpDwIcnTEx2ZeJkjI7Bmx9taZCBm4FNMxb/2tA8TqvfgHiKUiwthhpFw1qzjbp6OF/92lc9YB+82+IpZXhDYwkzWVxZnLtsFY2mcxDnJboxdE7GNda2nU1hHMKEMhHS2w5Qgc1Sk9MmOMuboOJEnnovv9tssdjl+DusLNo0hFef4KnqCNoOi7HnvAhpyQf9d3fgeRbvoJSAbCRbWUWLunOWEX712dB61KBJzQppBLhMhzekqphCaUKyzo6BSUXCpPqforJ9/5V9cLQplbmRzdHJlYW0KZW5kb2JqCjMwIDAgb2JqCjw8IC9MZW5ndGggMjQ5IC9GaWx0ZXIgL0ZsYXRlRGVjb2RlID4+CnN0cmVhbQp4nD1QO45EIQzrOYUv8CTyI3AeRqstZu/frgOaKVBMfrYzJNARgUcMMZSv4yWtoK6Bv4tC8W7i64PCIKtDUiDOeg+IdOymNpETOh2cMz9hN2OOwEUxBpzpdKY9ByY5+8IKhHMbZexWSCeJqiKO6jOOKZ4qe594FiztyDZbJ5I95CDhUlKJyaWflMo/bcqUCjpm0QQsErngZBNNOMu7SVKMGZQy6h6mdiJ9rDzIozroZE3OrCOZ2dNP25n4HHC3X9pkTpXHdB7M+Jy0zoM5Fbr344k2B02N2ujs9xNpKi9Sux1anX51EpXdGOcYEpdnfxnfZP/5B/6HWiIKZW5kc3RyZWFtCmVuZG9iagozMSAwIG9iago8PCAvTGVuZ3RoIDI0OSAvRmlsdGVyIC9GbGF0ZURlY29kZSA+PgpzdHJlYW0KeJxNUUmKAzAMu+cV+kAhXpO8p0OZQ+f/18oOhTkECa+Sk5aYWAsPMYQfLD34kSFzN/0bfqLZu1l6ksnZ/5jnIlNR+FKoLmJCXYgbz6ER8D2haxJZsb3xOSyjmXO+Bx+FuAQzoQFjfUkyuajmlSETTgx1HA5apMK4a2LD4lrRPI3cbvtGZmUmhA2PZELcGICIIOsCshgslDY2EzJZzgPtDckNWmDXqRtRi4IrlNYJdKJWxKrM4LPm1nY3Qy3y4Kh98fpoVpdghdFL9Vh4X4U+mKmZdu6SQnrhTTsizB4KpDI7LSu1e8TqboH6P8tS8P3J9/gdrw/N/FycCmVuZHN0cmVhbQplbmRvYmoKMzIgMCBvYmoKPDwgL0xlbmd0aCAzNDEgL0ZpbHRlciAvRmxhdGVEZWNvZGUgPj4Kc3RyZWFtCnicRVJLbkQxCNu/U3CBSOGXkPO0qrqY3n9bm0zVzeAJYGx4y1OmZMqwuSUjJNeUT30iQ6ym/DRyJCKm+EkJBXaVj8drS6yN7JGoFJ/a8eOx9Eam2RVa9e7Rpc2iUc3KyDnIEKGeFbqye9QO2fB6XEi675TNIRzL/1CBLGXdcgolQVvQd+wR3w8droIrgmGway6D7WUy1P/6hxZc7333YscugBas577BDgCopxO0BcgZ2u42KWgAVbqLScKj8npudqJso1Xp+RwAMw4wcsCIJVsdvtHeAJZ9XehFjYr9K0BRWUD8yNV2wd4xyUhwFuYGjr1wPMWZcEs4xgJAir3iGHrwJdjmL1euiJrwCXW6ZC+8wp7a5udCkwh3rQAOXmTDraujqJbt6TyC9mdFckaM1Is4OiGSWtI5guLSoB5a41w3seJtI7G5V9/uH+GcL1z26xdL7ITECmVuZHN0cmVhbQplbmRvYmoKMzMgMCBvYmoKPDwgL1R5cGUgL1hPYmplY3QgL1N1YnR5cGUgL0Zvcm0gL0JCb3ggWyAtMTAyMSAtNDYzIDE3OTQgMTIzMyBdIC9MZW5ndGggMzkKL0ZpbHRlciAvRmxhdGVEZWNvZGUgPj4Kc3RyZWFtCnic4zI0MFMwNjVVyOUyNzYCs3LALCNzIyALJItgQWQzuNIAFfMKfAplbmRzdHJlYW0KZW5kb2JqCjM0IDAgb2JqCjw8IC9MZW5ndGggODMgL0ZpbHRlciAvRmxhdGVEZWNvZGUgPj4Kc3RyZWFtCnicRYy7DcAwCER7pmAEfib2PlGUwt6/DRAlbrgn3T1cHQmZKW4zw0MGngwshl1xgfSWMAtcR1COneyjYdW+6gSN9aZS8+8PlJ7srOKG6wECQhpmCmVuZHN0cmVhbQplbmRvYmoKMzUgMCBvYmoKPDwgL0xlbmd0aCAxNTAgL0ZpbHRlciAvRmxhdGVEZWNvZGUgPj4Kc3RyZWFtCnicPU85DsMwDNv9Cn4ggHVYtt6TIuiQ/n+t6KAdBBGgeMiyo2MFDjGBSccciZe0H/w0jUAsg5ojekLFMCxwNkmBh0FWSVc+W5xMIbUFXkj41hQ8G01kgp7HiB24k8noA+9SW7F16AHtEFUkXbMMY7GtunA9YQQ1xXoV5vUwY4mSR59VS+sBBRP40vl/7m7vdn0BYMUwXQplbmRzdHJlYW0KZW5kb2JqCjM2IDAgb2JqCjw8IC9MZW5ndGggMTUxIC9GaWx0ZXIgL0ZsYXRlRGVjb2RlID4+CnN0cmVhbQp4nDWPyw3DMAxD75qCCwTQz7I8T4qgh3T/ayWnBQyYMMkn2RaDkYxDTGDsmGPhJVRPrT4kI7e6STkQqVA3BE9oTAwznKRL4JXpvmU8t3g5rdQFnZDI3VltNEQZzTyGo6fsFU76L3OTqJUZZQ7IrFPdTsjKghWYF9Ry38+4rXKhEx62K8OiO8WIcpsZafj976Q3XV/ceDDVCmVuZHN0cmVhbQplbmRvYmoKMzcgMCBvYmoKPDwgL0xlbmd0aCA1MSAvRmlsdGVyIC9GbGF0ZURlY29kZSA+PgpzdHJlYW0KeJwzNrRQMFAwNDAHkkaGQJaRiUKKIRdIAMTM5YIJ5oBZBkAaojgHriaHK4MrDQDhtA2YCmVuZHN0cmVhbQplbmRvYmoKMzggMCBvYmoKPDwgL0xlbmd0aCA3MCAvRmlsdGVyIC9GbGF0ZURlY29kZSA+PgpzdHJlYW0KeJwzMzZTMFCwMAISpqaGCuZGlgophlxAPoiVywUTywGzzCzMgSwjC5CWHC5DC2MwbWJspGBmYgZkWSAxILoyuNIAmJoTAwplbmRzdHJlYW0KZW5kb2JqCjM5IDAgb2JqCjw8IC9MZW5ndGggMTggL0ZpbHRlciAvRmxhdGVEZWNvZGUgPj4Kc3RyZWFtCnicMza0UDCAwxRDrjQAHeYDUgplbmRzdHJlYW0KZW5kb2JqCjQwIDAgb2JqCjw8IC9MZW5ndGggMjUxIC9GaWx0ZXIgL0ZsYXRlRGVjb2RlID4+CnN0cmVhbQp4nC1RSXIDQQi7zyv0hGan32OXK4fk/9cIygcGDYtAdFrioIyfICxXvOWRq2jD3zMxgt8Fh34r121Y5EBUIEljUDWhdvF69B7YcZgJzJPWsAxmrA/8jCnc6MXhMRlnt9dl1BDsXa89mUHJrFzEJRMXTNVhI2cOP5kyLrRzPTcg50ZYl2GQblYaMxKONIVIIYWqm6TOBEESjK5GjTZyFPulL490hlWNqDHscy1tX89NOGvQ7Fis8uSUHl1xLicXL6wc9PU2AxdRaazyQEjA/W4P9XOyk994S+fOFtPje83J8sJUYMWb125ANtXi37yI4/uMr+fn+fwDX2BbiAplbmRzdHJlYW0KZW5kb2JqCjQxIDAgb2JqCjw8IC9MZW5ndGggMjE1IC9GaWx0ZXIgL0ZsYXRlRGVjb2RlID4+CnN0cmVhbQp4nDVROQ4DIQzs9xX+QCSML3hPoijN/r/NjNFWHsFchrSUIZnyUpOoIeVTPnqZLpy63NfMajTnlrQtc4C4trwvrZLAiWaIg8FpmLgBmjwBQ9fRqFFDFx7Q1KVTKLDcBD6Kt24P3WO1gZe2IeeJIGIoGSxBzalFExZtzyekNb9eixvel+3dyFOlxpYYgQYBVjgc1+jX8JU9TybRdBUy1Ks1yxgJE0UiPPmOptUT61o00jIS1MYRrGoDvDv9ME4AABNxywJkn0qUs+TEb7H0swZX+v4Bn0dUlgplbmRzdHJlYW0KZW5kb2JqCjI2IDAgb2JqCjw8IC9UeXBlIC9Gb250IC9CYXNlRm9udCAvQk1RUURWK0RlamFWdVNhbnMgL0ZpcnN0Q2hhciAwIC9MYXN0Q2hhciAyNTUKL0ZvbnREZXNjcmlwdG9yIDI1IDAgUiAvU3VidHlwZSAvVHlwZTMgL05hbWUgL0JNUVFEVitEZWphVnVTYW5zCi9Gb250QkJveCBbIC0xMDIxIC00NjMgMTc5NCAxMjMzIF0gL0ZvbnRNYXRyaXggWyAwLjAwMSAwIDAgMC4wMDEgMCAwIF0KL0NoYXJQcm9jcyAyNyAwIFIKL0VuY29kaW5nIDw8IC9UeXBlIC9FbmNvZGluZwovRGlmZmVyZW5jZXMgWyAzMiAvc3BhY2UgNDAgL3BhcmVubGVmdCAvcGFyZW5yaWdodCA0NiAvcGVyaW9kIDQ4IC96ZXJvIC9vbmUgL3R3byA1MwovZml2ZSA1NSAvc2V2ZW4gMTAwIC9kIC9lIDEwMyAvZyAxOTcgL0FyaW5nIF0KPj4KL1dpZHRocyAyNCAwIFIgPj4KZW5kb2JqCjI1IDAgb2JqCjw8IC9UeXBlIC9Gb250RGVzY3JpcHRvciAvRm9udE5hbWUgL0JNUVFEVitEZWphVnVTYW5zIC9GbGFncyAzMgovRm9udEJCb3ggWyAtMTAyMSAtNDYzIDE3OTQgMTIzMyBdIC9Bc2NlbnQgOTI5IC9EZXNjZW50IC0yMzYgL0NhcEhlaWdodCAwCi9YSGVpZ2h0IDAgL0l0YWxpY0FuZ2xlIDAgL1N0ZW1WIDAgL01heFdpZHRoIDEzNDIgPj4KZW5kb2JqCjI0IDAgb2JqClsgNjAwIDYwMCA2MDAgNjAwIDYwMCA2MDAgNjAwIDYwMCA2MDAgNjAwIDYwMCA2MDAgNjAwIDYwMCA2MDAgNjAwIDYwMCA2MDAKNjAwIDYwMCA2MDAgNjAwIDYwMCA2MDAgNjAwIDYwMCA2MDAgNjAwIDYwMCA2MDAgNjAwIDYwMCAzMTggNDAxIDQ2MCA4MzggNjM2Cjk1MCA3ODAgMjc1IDM5MCAzOTAgNTAwIDgzOCAzMTggMzYxIDMxOCAzMzcgNjM2IDYzNiA2MzYgNjM2IDYzNiA2MzYgNjM2IDYzNgo2MzYgNjM2IDMzNyAzMzcgODM4IDgzOCA4MzggNTMxIDEwMDAgNjg0IDY4NiA2OTggNzcwIDYzMiA1NzUgNzc1IDc1MiAyOTUKMjk1IDY1NiA1NTcgODYzIDc0OCA3ODcgNjAzIDc4NyA2OTUgNjM1IDYxMSA3MzIgNjg0IDk4OSA2ODUgNjExIDY4NSAzOTAgMzM3CjM5MCA4MzggNTAwIDUwMCA2MTMgNjM1IDU1MCA2MzUgNjE1IDM1MiA2MzUgNjM0IDI3OCAyNzggNTc5IDI3OCA5NzQgNjM0IDYxMgo2MzUgNjM1IDQxMSA1MjEgMzkyIDYzNCA1OTIgODE4IDU5MiA1OTIgNTI1IDYzNiAzMzcgNjM2IDgzOCA2MDAgNjM2IDYwMCAzMTgKMzUyIDUxOCAxMDAwIDUwMCA1MDAgNTAwIDEzNDIgNjM1IDQwMCAxMDcwIDYwMCA2ODUgNjAwIDYwMCAzMTggMzE4IDUxOCA1MTgKNTkwIDUwMCAxMDAwIDUwMCAxMDAwIDUyMSA0MDAgMTAyMyA2MDAgNTI1IDYxMSAzMTggNDAxIDYzNiA2MzYgNjM2IDYzNiAzMzcKNTAwIDUwMCAxMDAwIDQ3MSA2MTIgODM4IDM2MSAxMDAwIDUwMCA1MDAgODM4IDQwMSA0MDEgNTAwIDYzNiA2MzYgMzE4IDUwMAo0MDEgNDcxIDYxMiA5NjkgOTY5IDk2OSA1MzEgNjg0IDY4NCA2ODQgNjg0IDY4NCA2ODQgOTc0IDY5OCA2MzIgNjMyIDYzMiA2MzIKMjk1IDI5NSAyOTUgMjk1IDc3NSA3NDggNzg3IDc4NyA3ODcgNzg3IDc4NyA4MzggNzg3IDczMiA3MzIgNzMyIDczMiA2MTEgNjA1CjYzMCA2MTMgNjEzIDYxMyA2MTMgNjEzIDYxMyA5ODIgNTUwIDYxNSA2MTUgNjE1IDYxNSAyNzggMjc4IDI3OCAyNzggNjEyIDYzNAo2MTIgNjEyIDYxMiA2MTIgNjEyIDgzOCA2MTIgNjM0IDYzNCA2MzQgNjM0IDU5MiA2MzUgNTkyIF0KZW5kb2JqCjI3IDAgb2JqCjw8IC9BcmluZyAyOCAwIFIgL2QgMjkgMCBSIC9lIDMwIDAgUiAvZml2ZSAzMSAwIFIgL2cgMzIgMCBSIC9vbmUgMzQgMCBSCi9wYXJlbmxlZnQgMzUgMCBSIC9wYXJlbnJpZ2h0IDM2IDAg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MDAwMDAgbiAKMDAwMDAxNzM5NiAwMDAwMCBuIAowMDAwMDAzNTUzIDAwMDAwIG4gCjAwMDAwMDMzMzggMDAwMDAgbiAKMDAwMDAwMjk5OCAwMDAwMCBuIAowMDAwMDA0NjA2IDAwMDAwIG4gCjAwMDAwMDE3MDcgMDAwMDAgbiAKMDAwMDAwMjExOCAwMDAwMCBuIAowMDAwMDAyNDU2IDAwMDAwIG4gCjAwMDAwMDI2MTcgMDAwMDAgbiAKMDAwMDAwMjc4NCAwMDAwMCBuIAowMDAwMDA4NzMxIDAwMDAwIG4gCjAwMDAwMDg1MjQgMDAwMDAgbiAKMDAwMDAwODEwNCAwMDAwMCBuIAowMDAwMDA5Nzg0IDAwMDAwIG4gCjAwMDAwMDQ2NTggMDAwMDAgbiAKMDAwMDAwNTAwMSAwMDAwMCBuIAowMDAwMDA1MzA1IDAwMDAwIG4gCjAwMDAwMDU2MjcgMDAwMDAgbiAKMDAwMDAwNTk0OSAwMDAwMCBuIAowMDAwMDA2MzYzIDAwMDAwIG4gCjAwMDAwMDY1MzUgMDAwMDAgbiAKMDAwMDAwNjY5MCAwMDAwMCBuIAowMDAwMDA2OTEzIDAwMDAwIG4gCjAwMDAwMDcxMzcgMDAwMDAgbiAKMDAwMDAwNzI2MCAwMDAwMCBuIAowMDAwMDA3NDAyIDAwMDAwIG4gCjAwMDAwMDc0OTIgMDAwMDAgbiAKMDAwMDAwNzgxNiAwMDAwMCBuIAowMDAwMDE2NzkwIDAwMDAwIG4gCjAwMDAwMTY4MTEgMDAwMDAgbiAKMDAwMDAxNzM3NiAwMDAwMCBuIAowMDAwMDMyNzgwIDAwMDAwIG4gCjAwMDAwMzI4NjIgMDAwMDAgbiAKdHJhaWxlcgo8PCAvU2l6ZSA0NyAvUm9vdCAxIDAgUiAvSW5mbyA0NiAwIFIgPj4Kc3RhcnR4cmVmCjMzMDE5CiUlRU9GCg==", - "image/svg+xml": [ - "\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " 2024-04-22T21:56:55.868237\n", - " image/svg+xml\n", - " \n", - " \n", - " Matplotlib v3.8.4, https://matplotlib.org/\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "\n" - ], - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "fig, axs = plt.subplots(1, 2, layout=\"compressed\")\n", "eplt.plot_array(dat.sel(eV=-0.3, method=\"nearest\"), ax=axs[0], aspect=\"equal\")\n", @@ -2709,23 +216,9 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
delta0.0
xi0.0
beta0.0
" - ], - "text/plain": [ - "{'delta': 0.0, 'xi': 0.0, 'beta': 0.0}" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "dat.kspace.offsets" ] @@ -2739,516 +232,18 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
delta60.0
xi0.0
beta30.0
" - ], - "text/plain": [ - "{'delta': 60.0, 'xi': 0.0, 'beta': 30.0}" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "dat.kspace.offsets.update(delta=60.0, beta=30.0)" ] }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Estimating bounds and resolution\n", - "Calculating destination coordinates\n", - "Converting ('eV', 'alpha', 'beta') -> ('eV', 'kx', 'ky')\n", - "Interpolated in 0.494 s\n" - ] - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "
<xarray.DataArray (eV: 500, kx: 380, ky: 398)>\n",
-       "nan nan nan nan nan nan nan nan nan nan ... nan nan nan nan nan nan nan nan nan\n",
-       "Coordinates:\n",
-       "    xi       float64 0.0\n",
-       "    delta    float64 0.0\n",
-       "    hv       float64 50.0\n",
-       "  * eV       (eV) float64 -0.45 -0.4489 -0.4477 -0.4466 ... 0.1177 0.1189 0.12\n",
-       "  * kx       (kx) float64 -2.495 -2.489 -2.483 ... -0.3111 -0.3053 -0.2995\n",
-       "  * ky       (ky) float64 -0.3431 -0.3373 -0.3315 -0.3257 ... 1.946 1.952 1.957\n",
-       "Attributes:\n",
-       "    configuration:        1\n",
-       "    temp_sample:          20.0\n",
-       "    sample_workfunction:  4.5\n",
-       "    delta_offset:         60.0\n",
-       "    xi_offset:            0.0\n",
-       "    beta_offset:          30.0
" - ], - "text/plain": [ - "\n", - "nan nan nan nan nan nan nan nan nan nan ... nan nan nan nan nan nan nan nan nan\n", - "Coordinates:\n", - " xi float64 0.0\n", - " delta float64 0.0\n", - " hv float64 50.0\n", - " * eV (eV) float64 -0.45 -0.4489 -0.4477 -0.4466 ... 0.1177 0.1189 0.12\n", - " * kx (kx) float64 -2.495 -2.489 -2.483 ... -0.3111 -0.3053 -0.2995\n", - " * ky (ky) float64 -0.3431 -0.3373 -0.3315 -0.3257 ... 1.946 1.952 1.957\n", - "Attributes:\n", - " configuration: 1\n", - " temp_sample: 20.0\n", - " sample_workfunction: 4.5\n", - " delta_offset: 60.0\n", - " xi_offset: 0.0\n", - " beta_offset: 30.0" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "dat_kconv = dat.kspace.convert()\n", "dat_kconv" @@ -3263,818 +258,9 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 10, - "metadata": {}, - "output_type": "execute_result" - }, - { - "data": { - "application/pdf": "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IDYwMCA2MDAgNjAwIDYwMCA2MDAgNjAwIDYwMCA2MDAgNjAwIDMxOCA0MDEgNDYwIDgzOCA2MzYKOTUwIDc4MCAyNzUgMzkwIDM5MCA1MDAgODM4IDMxOCAzNjEgMzE4IDMzNyA2MzYgNjM2IDYzNiA2MzYgNjM2IDYzNiA2MzYgNjM2CjYzNiA2MzYgMzM3IDMzNyA4MzggODM4IDgzOCA1MzEgMTAwMCA2ODQgNjg2IDY5OCA3NzAgNjMyIDU3NSA3NzUgNzUyIDI5NQoyOTUgNjU2IDU1NyA4NjMgNzQ4IDc4NyA2MDMgNzg3IDY5NSA2MzUgNjExIDczMiA2ODQgOTg5IDY4NSA2MTEgNjg1IDM5MCAzMzcKMzkwIDgzOCA1MDAgNTAwIDYxMyA2MzUgNTUwIDYzNSA2MTUgMzUyIDYzNSA2MzQgMjc4IDI3OCA1NzkgMjc4IDk3NCA2MzQgNjEyCjYzNSA2MzUgNDExIDUyMSAzOTIgNjM0IDU5MiA4MTggNTkyIDU5MiA1MjUgNjM2IDMzNyA2MzYgODM4IDYwMCA2MzYgNjAwIDMxOAozNTIgNTE4IDEwMDAgNTAwIDUwMCA1MDAgMTM1MCA2MzUgNDAwIDEwNzAgNjAwIDY4NSA2MDAgNjAwIDMxOCAzMTggNTE4IDUxOAo1OTAgNTAwIDEwMDAgNTAwIDEwMDAgNTIxIDQwMCAxMDI4IDYwMCA1MjUgNjExIDMxOCA0MDEgNjM2IDYzNiA2MzYgNjM2IDMzNwo1MDAgNTAwIDEwMDAgNDcxIDYxNyA4MzggMzYxIDEwMDAgNTAwIDUwMCA4MzggNDAxIDQwMSA1MDAgNjM2IDYzNiAzMTggNTAwCjQwMSA0NzEgNjE3IDk2OSA5NjkgOTY5IDUzMSA2ODQgNjg0IDY4NCA2ODQgNjg0IDY4NCA5NzQgNjk4IDYzMiA2MzIgNjMyIDYzMgoyOTUgMjk1IDI5NSAyOTUgNzc1IDc0OCA3ODcgNzg3IDc4NyA3ODcgNzg3IDgzOCA3ODcgNzMyIDczMiA3MzIgNzMyIDYxMSA2MDgKNjMwIDYxMyA2MTMgNjEzIDYxMyA2MTMgNjEzIDk5NSA1NTAgNjE1IDYxNSA2MTUgNjE1IDI3OCAyNzggMjc4IDI3OCA2MTIgNjM0CjYxMiA2MTIgNjEyIDYx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MC4wMDEgMCAwIDAuMDAxIDAgMCBdCi9DaGFyUHJvY3MgMjcgMCBSCi9FbmNvZGluZyA8PCAvVHlwZSAvRW5jb2RpbmcKL0RpZmZlcmVuY2VzIFsgMzIgL3NwYWNlIDQwIC9wYXJlbmxlZnQgL3BhcmVucmlnaHQgNDYgL3BlcmlvZCA0OCAvemVybyAvb25lIC90d28gNTMKL2ZpdmUgMTAwIC9kIC9lIDEwMyAvZyAxOTcgL0FyaW5nIF0KPj4KL1dpZHRocyAyNCAwIFIgPj4KZW5kb2JqCjI1IDAgb2JqCjw8IC9UeXBlIC9Gb250RGVzY3JpcHRvciAvRm9udE5hbWUgL0JNUVFEVitEZWphVnVTYW5zIC9GbGFncyAzMgovRm9udEJCb3ggWyAtMTAyMSAtNDYzIDE3OTQgMTIzMyBdIC9Bc2NlbnQgOTI5IC9EZXNjZW50IC0yMzYgL0NhcEhlaWdodCAwCi9YSGVpZ2h0IDAgL0l0YWxpY0FuZ2xlIDAgL1N0ZW1WIDAgL01heFdpZHRoIDEzNDIgPj4KZW5kb2JqCjI0IDAgb2JqClsgNjAwIDYwMCA2MDAgNjAwIDYwMCA2MDAgNjAwIDYwMCA2MDAgNjAwIDYwMCA2MDAgNjAwIDYwMCA2MDAgNjAwIDYwMCA2MDAKNjAwIDYwMCA2MDAgNjAwIDYwMCA2MDAgNjAwIDYwMCA2MDAgNjAwIDYwMCA2MDAgNjAwIDYwMCAzMTggNDAxIDQ2MCA4MzggNjM2Cjk1MCA3ODAgMjc1IDM5MCAzOTAgNTAwIDgzOCAzMTggMzYxIDMxOCAzMzcgNjM2IDYzNiA2MzYgNjM2IDYzNiA2MzYgNjM2IDYzNgo2MzYgNjM2IDMzNyAzMzcgODM4IDgzOCA4MzggNTMxIDEwMDAgNjg0IDY4NiA2OTggNzcwIDYzMiA1NzUgNzc1IDc1MiAyOTUKMjk1IDY1NiA1NTcgODYzIDc0OCA3ODcgNjAzIDc4NyA2OTUgNjM1IDYxMSA3MzIgNjg0IDk4OSA2ODUgNjExIDY4NSAzOTAgMzM3CjM5MCA4MzggNTAwIDUwMCA2MTMgNjM1IDU1MCA2MzUgNjE1IDM1MiA2MzUgNjM0IDI3OCAyNzggNTc5IDI3OCA5NzQgNjM0IDYxMgo2MzUgNjM1IDQxMSA1MjEgMzkyIDYzNCA1OTIgODE4IDU5MiA1OTIgNTI1IDYzNiAzMzcgNjM2IDgzOCA2MDAgNjM2IDYwMCAzMTgKMzUyIDUxOCAxMDAwIDUwMCA1MDAgNTAwIDEzNDIgNjM1IDQwMCAxMDcwIDYwMCA2ODUgNjAwIDYwMCAzMTggMzE4IDUxOCA1MTgKNTkwIDUwMCAxMDAwIDUwMCAxMDAwIDUyMSA0MDAgMTAyMyA2MDAgNTI1IDYxMSAzMTggNDAxIDYzNiA2MzYgNjM2IDYzNiAzMzcKNTAwIDUwMCAxMDAwIDQ3MSA2MTIgODM4IDM2MSAxMDAwIDUwMCA1MDAgODM4IDQwMSA0MDEgNTAwIDYzNiA2MzYgMzE4IDUwMAo0MDEgNDcxIDYxMiA5NjkgOTY5IDk2OSA1MzEgNjg0IDY4NCA2ODQgNjg0IDY4NCA2ODQgOTc0IDY5OCA2MzIgNjMyIDYzMiA2MzIKMjk1IDI5NSAyOTUgMjk1IDc3NSA3NDggNzg3IDc4NyA3ODcgNzg3IDc4NyA4MzggNzg3IDczMiA3MzIgNzMyIDczMiA2MTEgNjA1CjYzMCA2MTMgNjEzIDYxMyA2MTMgNjEzIDYxMyA5ODIgNTUwIDYxNSA2MTUgNjE1IDYxNSAyNzggMjc4IDI3OCAyNzggNjEyIDYzNAo2MTIgNjEyIDYxMiA2MTIgNjEyIDgzOCA2MTIgNjM0IDYzNCA2MzQgNjM0IDU5MiA2MzUgNTkyIF0KZW5kb2JqCjI3IDAgb2JqCjw8IC9BcmluZyAyOCAwIFIgL2QgMjkgMCBSIC9lIDMwIDAgUiAvZml2ZSAzMSAwIFIgL2cgMzIgMCBSIC9vbmUgMzQgMCBSCi9wYXJlbmxlZnQgMzUgMCBSIC9wYXJlbnJpZ2h0IDM2IDAgUiAvcGVyaW9kIDM3IDAgUiAvc3BhY2UgMzggMCBSCi90d28gMzkgMCBSIC96ZXJvIDQwIDAgUiA+PgplbmRvYmoKMyAwIG9iago8PCAvRjIgMTcgMCBSIC9GMSAyNiAwIFIgPj4KZW5kb2JqCjQgMCBvYmoKPDwgL0ExIDw8IC9UeXBlIC9FeHRHU3RhdGUgL0NBIDAgL2NhIDEgPj4KL0EyIDw8IC9UeXBlIC9FeHRHU3RhdGUgL0NBIDEgL2NhIDEgPj4gPj4KZW5kb2Jq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MCBuIAowMDAwMDA0NTUxIDAwMDAwIG4gCjAwMDAwMDE2NTIgMDAwMDAgbiAKMDAwMDAwMjA2MyAwMDAwMCBuIAowMDAwMDAyNDAxIDAwMDAwIG4gCjAwMDAwMDI1NjIgMDAwMDAgbiAKMDAwMDAwMjcyOSAwMDAwMCBuIAowMDAwMDA4NTI0IDAwMDAwIG4gCjAwMDAwMDgzMTcgMDAwMDAgbiAKMDAwMDAwNzkwNyAwMDAwMCBuIAowMDAwMDA5NTc3IDAwMDAwIG4gCjAwMDAwMDQ2MDMgMDAwMDAgbiAKMDAwMDAwNDk0NiAwMDAwMCBuIAowMDAwMDA1MjUwIDAwMDAwIG4gCjAwMDAwMDU1NzIgMDAwMDAgbiAKMDAwMDAwNTg5NCAwMDAwMCBuIAowMDAwMDA2MzA4IDAwMDAwIG4gCjAwMDAwMDY0ODAgMDAwMDAgbiAKMDAwMDAwNjYzNSAwMDAwMCBuIAowMDAwMDA2ODU4IDAwMDAwIG4gCjAwMDAwMDcwODIgMDAwMDAgbiAKMDAwMDAwNzIwNSAwMDAwMCBuIAowMDAwMDA3Mjk1IDAwMDAwIG4gCjAwMDAwMDc2MTkgMDAwMDAgbiAKMDAwMDAxNjQyNSAwMDAwMCBuIAowMDAwMDE2NDQ2IDAwMDAwIG4gCjAwMDAwMTgwNTEgMDAwMDAgbiAKMDAwMDAyOTk5NiAwMDAwMCBuIAowMDAwMDMwMDc4IDAwMDAwIG4gCnRyYWlsZXIKPDwgL1NpemUgNDYgL1Jvb3QgMSAwIFIgL0luZm8gNDUgMCBSID4+CnN0YXJ0eHJlZgozMDIzNQolJUVPRgo=", - "image/svg+xml": [ - "\n", - "\n", - "\n", - " \n", - " \n", - " \n", - " \n", - " 2024-04-22T21:57:01.645776\n", - " image/svg+xml\n", - " \n", - " \n", - " Matplotlib v3.8.4, https://matplotlib.org/\n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "\n" - ], - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "fig, axs = plt.subplots(1, 2, layout=\"compressed\")\n", "eplt.plot_array(dat.sel(eV=-0.3, method=\"nearest\"), ax=axs[0], aspect=\"equal\")\n", @@ -4177,7 +363,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.8" + "version": "3.11.9" } }, "nbformat": 4, diff --git a/docs/source/user-guide/plotting.ipynb b/docs/source/user-guide/plotting.ipynb index 0b9801cf..b0ed0369 100644 --- a/docs/source/user-guide/plotting.ipynb +++ b/docs/source/user-guide/plotting.ipynb @@ -85,7 +85,7 @@ "source": [ "from erlab.io.exampledata import generate_data\n", "\n", - "dat = generate_data(bandshift=-0.2).T" + "dat = generate_data(bandshift=-0.2, seed=1).T" ] }, { @@ -494,14 +494,18 @@ "metadata": {}, "outputs": [], "source": [ - "dat0, dat1 = generate_data(shape=(250, 250, 2), Erange=(-0.3, 0.3), temp=0.0).T\n", + "dat0, dat1 = generate_data(\n", + " shape=(250, 250, 2), Erange=(-0.3, 0.3), temp=0.0, seed=1, count=1e6\n", + ").T\n", "\n", - "eplt.plot_slices(\n", + "_, axs = eplt.plot_slices(\n", " [dat0, dat1],\n", " order=\"F\",\n", " subplot_kw={\"layout\": \"compressed\", \"sharey\": \"row\"},\n", " axis=\"scaled\",\n", - ")" + " label=True,\n", + ")\n", + "# eplt.label_subplot_properties(axs, values=dict(Eb=[-0.3, 0.3]))" ] }, { @@ -517,11 +521,11 @@ "metadata": {}, "outputs": [], "source": [ - "lightness = dat0 + dat1\n", - "color = (dat0 - dat1) / lightness\n", + "dat_sum = dat0 + dat1\n", + "dat_ndiff = (dat0 - dat1) / dat_sum\n", "\n", "eplt.plot_slices(\n", - " [lightness, color],\n", + " [dat_sum, dat_ndiff],\n", " order=\"F\",\n", " subplot_kw={\"layout\": \"compressed\", \"sharey\": \"row\"},\n", " cmap=[\"viridis\", \"bwr\"],\n", @@ -534,7 +538,27 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "The difference array is noisy for small values of the sum. We can plot using a 2D colomap to visualize the relevant features better." + "The difference array is noisy for small values of the sum. We can plot using a 2D\n", + "colomap, where `dat_ndiff` is mapped to the color along the colormap and `dat_sum` is\n", + "mapped to the lightness of the colormap." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "eplt.plot_array_2d(dat_sum, dat_ndiff)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The color normalization for each axis can be set independently with `lnorm` and `cnorm`.\n", + "The appearance of the colorbar axes can be customized with the returned `Colorbar`\n", + "object." ] }, { @@ -544,13 +568,13 @@ "outputs": [], "source": [ "_, cb = eplt.plot_array_2d(\n", - " lightness,\n", - " color,\n", + " dat_sum,\n", + " dat_ndiff,\n", " lnorm=eplt.InversePowerNorm(0.5),\n", " cnorm=eplt.CenteredInversePowerNorm(0.7, vcenter=0.0, halfrange=1.0),\n", ")\n", - "cb.ax.set_xticks([])\n", - "eplt.fancy_labels()" + "cb.ax.set_xticks(cb.ax.get_xlim())\n", + "cb.ax.set_xticklabels([\"Min\", \"Max\"])" ] }, { @@ -637,7 +661,7 @@ "source": [ "import hvplot.xarray\n", "\n", - "cut.hvplot(x=\"kx\", y=\"eV\", cmap=\"Greys\")" + "cut.hvplot(x=\"kx\", y=\"eV\", cmap=\"Greys\", aspect=1.5)" ] }, { @@ -646,7 +670,7 @@ "metadata": {}, "outputs": [], "source": [ - "dat.hvplot(x=\"kx\", y=\"ky\", cmap=\"Greys\", widget_location=\"bottom\")" + "dat.hvplot(x=\"kx\", y=\"ky\", cmap=\"Greys\", aspect=\"equal\", widget_location=\"bottom\")" ] }, { @@ -689,7 +713,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.8" + "version": "3.11.9" } }, "nbformat": 4, diff --git a/environment.yml b/environment.yml index d3c38ab5..40ae75cd 100644 --- a/environment.yml +++ b/environment.yml @@ -4,7 +4,6 @@ channels: dependencies: - h5netcdf>=1.2.0 - igor2>=0.5.6 - - iminuit>=2.25.2 - ipykernel - joblib>=1.3.2 - lmfit>=1.2.0,!=1.3.0 @@ -19,9 +18,8 @@ dependencies: - qtawesome>=1.3.1 - qtpy>=2.4.1 - scipy>=1.12.0 - - superqt>=0.6.2 - tqdm>=4.66.2 - - uncertainties>=3.0.1 + - uncertainties>=3.1.4 - varname>=0.13.0 - xarray>=2024.02.0 - pip: diff --git a/pyproject.toml b/pyproject.toml index 628ab6a6..5c93ebab 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -24,7 +24,6 @@ dynamic = ["version"] dependencies = [ "h5netcdf>=1.2.0", "igor2>=0.5.6", - "iminuit>=2.25.2", "joblib>=1.3.2", "lmfit>=1.2.0,!=1.3.0", "matplotlib>=3.8.0", @@ -36,9 +35,8 @@ dependencies = [ "qtawesome>=1.3.1", "qtpy>=2.4.1", "scipy>=1.12.0", - "superqt>=0.6.2", "tqdm>=4.66.2", - "uncertainties>=3.0.1", + "uncertainties>=3.1.4", "varname>=0.13.0", "xarray>=2024.02.0", ] @@ -156,10 +154,30 @@ select = [ "PERF", "RUF", ] -ignore = ["B905", "ICN001", "TRY003", "RUF001", "RUF002", "RUF003", "RUF012"] +ignore = [ + "ICN001", # Import conventions + "TRY003", # Long exception messages +] extend-select = [ "UP", # pyupgrade ] +allowed-confusables = [ + "×", + "−", + "𝑎", + "𝒂", + "𝑏", + "𝒃", + "𝑐", + "𝑥", + "𝑦", + "𝑧", + "𝛼", + "γ", + "𝛾", + "ν", + "α", +] [tool.ruff.format] quote-style = "double" @@ -176,3 +194,37 @@ profile = "black" addopts = ["--import-mode=importlib"] pythonpath = "src" testpaths = "tests" + +[tool.mypy] +plugins = ["numpy.typing.mypy_plugin"] +warn_unused_configs = true +warn_redundant_casts = true +warn_unused_ignores = true +allow_redefinition = true +exclude = [ + "^docs/", + "^tests/", + "_deprecated/", + "interactive/fermiedge.py", + "io/", +] + +[[tool.mypy.overrides]] +module = [ + "astropy.*", + "h5netcdf.*", + "igor2.*", + "iminuit.*", + "ipywidgets.*", + "joblib.*", + "lmfit.*", + "mpl_toolkits.*", + "numba.*", + "pyperclip.*", + "pyqtgraph.*", + "qtawesome.*", + "scipy.*", + "uncertainties.*", + "varname.*", +] +ignore_missing_imports = true diff --git a/requirements.txt b/requirements.txt index ce4088e1..40a1c36d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,6 @@ dask>=2024.4.1 h5netcdf>=1.2.0 igor2>=0.5.6 -iminuit>=2.25.2 joblib>=1.3.2 lmfit>=1.2.0,!=1.3.0 matplotlib>=3.8.0 @@ -14,8 +13,7 @@ pyqtgraph>=0.13.1 qtawesome>=1.3.1 qtpy>=2.4.1 scipy>=1.12.0 -superqt>=0.6.2 tqdm>=4.66.2 -uncertainties>=3.0.1 +uncertainties>=3.1.4 varname>=0.13.0 xarray>=2024.02.0 diff --git a/src/erlab/accessors/fit.py b/src/erlab/accessors/fit.py index eaa540d7..094aefcd 100644 --- a/src/erlab/accessors/fit.py +++ b/src/erlab/accessors/fit.py @@ -18,7 +18,11 @@ import tqdm.auto import xarray as xr -from erlab.accessors.utils import _THIS_ARRAY, ERLabAccessor +from erlab.accessors.utils import ( + _THIS_ARRAY, + ERLabDataArrayAccessor, + ERLabDatasetAccessor, +) from erlab.parallel import joblib_progress if TYPE_CHECKING: @@ -33,7 +37,7 @@ def _nested_dict_vals(d): yield v -def _broadcast_dict_values(d: Mapping[str, Any]) -> Mapping[str, xr.DataArray]: +def _broadcast_dict_values(d: dict[str, Any]) -> dict[str, xr.DataArray]: to_broadcast = {} for k, v in d.items(): if isinstance(v, xr.DataArray | xr.Dataset): @@ -41,16 +45,20 @@ def _broadcast_dict_values(d: Mapping[str, Any]) -> Mapping[str, xr.DataArray]: else: to_broadcast[k] = xr.DataArray(v) - for k, v in zip(to_broadcast.keys(), xr.broadcast(*to_broadcast.values())): + for k, v in zip( + to_broadcast.keys(), xr.broadcast(*to_broadcast.values()), strict=True + ): d[k] = v return d -def _concat_along_keys(d: Mapping[str, xr.DataArray], dim_name: str) -> xr.DataArray: +def _concat_along_keys(d: dict[str, xr.DataArray], dim_name: str) -> xr.DataArray: return xr.concat(d.values(), d.keys()).rename(concat_dim=dim_name) -def _parse_params(d: Mapping[str, Any], dask: bool) -> xr.DataArray | _ParametersWraper: +def _parse_params( + d: dict[str, Any] | lmfit.Parameters, dask: bool +) -> xr.DataArray | _ParametersWraper: if isinstance(d, lmfit.Parameters): # Input to apply_ufunc cannot be a Mapping, so wrap in a class return _ParametersWraper(d) @@ -65,7 +73,7 @@ def _parse_params(d: Mapping[str, Any], dask: bool) -> xr.DataArray | _Parameter return _ParametersWraper(lmfit.create_params(**d)) -def _parse_multiple_params(d: Mapping[str, Any], as_str: bool) -> xr.DataArray: +def _parse_multiple_params(d: dict[str, Any], as_str: bool) -> xr.DataArray: for k in d.keys(): if isinstance(d[k], int | float | complex | xr.DataArray): d[k] = {"value": d[k]} @@ -111,7 +119,7 @@ def __init__(self, params: lmfit.Parameters): @xr.register_dataset_accessor("modelfit") -class ModelFitDatasetAccessor(ERLabAccessor): +class ModelFitDatasetAccessor(ERLabDatasetAccessor): """`xarray.Dataset.modelfit` accessor for fitting lmfit models.""" def __call__( @@ -121,9 +129,10 @@ def __call__( reduce_dims: Dims = None, skipna: bool = True, params: lmfit.Parameters - | Mapping[str, float | dict[str, Any]] + | dict[str, float | dict[str, Any]] | xr.DataArray | xr.Dataset + | _ParametersWraper | None = None, guess: bool = False, errors: Literal["raise", "ignore"] = "raise", @@ -374,10 +383,15 @@ def _wrapper(Y, *args, **kwargs): x = np.squeeze(x) - if n_coords == 1: - indep_var_kwargs = {model.independent_vars[0]: x} + if model.independent_vars is not None: + if n_coords == 1: + indep_var_kwargs = {model.independent_vars[0]: x} + else: + indep_var_kwargs = dict( + zip(model.independent_vars[:n_coords], x, strict=True) + ) else: - indep_var_kwargs = dict(zip(model.independent_vars[:n_coords], x)) + raise ValueError("Independent variables not defined in model") if guess: if isinstance(model, lmfit.model.CompositeModel): @@ -534,7 +548,7 @@ def _output_wrapper(name, da, out=None) -> dict: parallel_obj = joblib.Parallel(**parallel_kw) if parallel_obj.return_generator: - out_dicts = tqdm.auto.tqdm( + out_dicts = tqdm.auto.tqdm( # type: ignore[call-overload] parallel_obj( joblib.delayed(_output_wrapper)(name, da) for name, da in self._obj.data_vars.items() @@ -548,13 +562,13 @@ def _output_wrapper(name, da, out=None) -> dict: for name, da in self._obj.data_vars.items() ) result = type(self._obj)( - dict(itertools.chain.from_iterable(d.items() for d in out_dicts)) + dict(itertools.chain.from_iterable(d.items() for d in out_dicts)) # type: ignore[call-overload] ) del out_dicts else: result = type(self._obj)() - for name, da in tqdm.auto.tqdm(self._obj.data_vars.items(), **tqdm_kw): + for name, da in tqdm.auto.tqdm(self._obj.data_vars.items(), **tqdm_kw): # type: ignore[call-overload] _output_wrapper(name, da, result) result = result.assign_coords( @@ -572,23 +586,22 @@ def _output_wrapper(name, da, out=None) -> dict: @xr.register_dataarray_accessor("modelfit") -class ModelFitDataArrayAccessor(ERLabAccessor): +class ModelFitDataArrayAccessor(ERLabDataArrayAccessor): """`xarray.DataArray.modelfit` accessor for fitting lmfit models.""" def __call__(self, *args, **kwargs) -> xr.Dataset: return self._obj.to_dataset(name=_THIS_ARRAY).modelfit(*args, **kwargs) __call__.__doc__ = ( - ModelFitDatasetAccessor.__call__.__doc__.replace( - "Dataset.curvefit", "DataArray.curvefit" - ) + str(ModelFitDatasetAccessor.__call__.__doc__) + .replace("Dataset.curvefit", "DataArray.curvefit") .replace("Dataset.polyfit", "DataArray.polyfit") .replace("[var]_", "") ) @xr.register_dataarray_accessor("parallel_fit") -class ParallelFitDataArrayAccessor(ERLabAccessor): +class ParallelFitDataArrayAccessor(ERLabDataArrayAccessor): """ `xarray.DataArray.parallel_fit` accessor for fitting lmfit models in parallel along a single dimension. @@ -647,7 +660,7 @@ def __call__(self, dim: str, model: lmfit.Model, **kwargs) -> xr.Dataset: fitres = ds.modelfit(set(self._obj.dims) - {dim}, model, **kwargs) drop_keys = [] - concat_vars = {} + concat_vars: dict[Hashable, list[xr.DataArray]] = {} for k in ds.data_vars.keys(): for var in self._VAR_KEYS: key = f"{k}_{var}" diff --git a/src/erlab/accessors/kspace.py b/src/erlab/accessors/kspace.py index 13b6eebd..ab950519 100644 --- a/src/erlab/accessors/kspace.py +++ b/src/erlab/accessors/kspace.py @@ -6,20 +6,20 @@ import functools import time import warnings -from collections.abc import Callable, ItemsView, Iterable, Iterator -from typing import Literal +from collections.abc import Hashable, ItemsView, Iterable, Iterator, Mapping +from typing import Literal, cast import numpy as np import xarray as xr -from erlab.accessors.utils import ERLabAccessor +from erlab.accessors.utils import ERLabDataArrayAccessor from erlab.analysis.interpolate import interpn from erlab.analysis.kspace import AxesConfiguration, get_kconv_func, kz_func from erlab.constants import rel_kconv, rel_kzconv -from erlab.interactive.kspace import ktool +from erlab.interactive.kspace import KspaceTool, ktool -def only_angles(method: Callable | None = None): +def only_angles(method=None): """ A decorator that ensures the data is in angle space before executing the decorated method. @@ -28,7 +28,7 @@ def only_angles(method: Callable | None = None): `ValueError` is raised. """ - def wrapper(method: Callable): + def wrapper(method): @functools.wraps(method) def _impl(self, *args, **kwargs): if "kx" in self._obj.dims or "ky" in self._obj.dims: @@ -44,7 +44,7 @@ def _impl(self, *args, **kwargs): return wrapper -def only_momentum(method: Callable | None = None): +def only_momentum(method=None): """ A decorator that ensures the data is in momentum space before executing the decorated method. @@ -53,7 +53,7 @@ def only_momentum(method: Callable | None = None): present), a `ValueError` is raised. """ - def wrapper(method: Callable): + def wrapper(method): @functools.wraps(method) def _impl(self, *args, **kwargs): if not ("kx" in self._obj.dims or "ky" in self._obj.dims): @@ -111,10 +111,7 @@ def __init__(self, xarray_obj: xr.DataArray): if k + "_offset" not in self._obj.attrs: self[k] = 0.0 - def __len__(self) -> int: - return len(self._obj.kspace.valid_offset_keys) - - def __iter__(self) -> Iterator[str, float]: + def __iter__(self) -> Iterator[tuple[str, float]]: for key in self._obj.kspace.valid_offset_keys: yield key, self.__getitem__(key) @@ -132,7 +129,10 @@ def __setitem__(self, key: str, value: float) -> None: self._obj.attrs[key + "_offset"] = float(value) def __eq__(self, other: object) -> bool: - return dict(self) == dict(other) + if isinstance(other, Mapping): + return dict(self) == dict(other) + else: + return False def __repr__(self) -> str: return dict(self).__repr__() @@ -152,13 +152,13 @@ def _repr_html_(self) -> str: def update( self, - other: dict | Iterable[tuple[str, float]] | None = None, + other: dict[str, float] | Iterable[tuple[str, float]] | None = None, **kwargs, ) -> "OffsetView": """Updates the offset view with the provided key-value pairs.""" if other is not None: for k, v in other.items() if isinstance(other, dict) else other: - self[k] = v + self[str(k)] = v for k, v in kwargs.items(): self[k] = v return self @@ -175,7 +175,7 @@ def reset(self) -> "OffsetView": @xr.register_dataarray_accessor("kspace") -class MomentumAccessor(ERLabAccessor): +class MomentumAccessor(ERLabDataArrayAccessor): """`xarray.DataArray.kspace` accessor for momentum conversion related utilities. This class provides convenient access to various momentum-related properties of a @@ -198,7 +198,7 @@ def configuration(self) -> AxesConfiguration: "Configuration not found in data attributes! " "Data attributes may have been discarded since initial import." ) - return AxesConfiguration(int(self._obj.attrs.get("configuration"))) + return AxesConfiguration(int(self._obj.attrs.get("configuration", 0))) @configuration.setter def configuration(self, value: AxesConfiguration | int): @@ -294,7 +294,7 @@ def angle_resolution(self, value: float): self._obj.attrs["angle_resolution"] = float(value) @property - def slit_axis(self) -> str: + def slit_axis(self) -> Literal["kx", "ky"]: """Returns the momentum axis parallel to the slit. Returns @@ -309,7 +309,7 @@ def slit_axis(self) -> str: return "ky" @property - def other_axis(self) -> str: + def other_axis(self) -> Literal["kx", "ky"]: """Returns the momentum axis perpendicular to the slit. Returns @@ -325,7 +325,7 @@ def other_axis(self) -> str: @property @only_angles - def momentum_axes(self) -> tuple[str, ...]: + def momentum_axes(self) -> tuple[Literal["kx", "ky", "kz"], ...]: """Returns the momentum axes of the data after conversion. Returns @@ -529,9 +529,9 @@ def best_kz_resolution(self) -> float: kin = self.kinetic_energy.values c1, c2 = 641.0, 0.096 imfp = (c1 / (kin**2) + c2 * np.sqrt(kin)) * 10 - return np.amin(1 / imfp) + return float(np.amin(1 / imfp)) - def _get_transformed_coords(self) -> dict[str, xr.DataArray]: + def _get_transformed_coords(self) -> dict[Literal["kx", "ky", "kz"], xr.DataArray]: kx, ky = self._forward_func(self.alpha, self.beta) if "hv" in kx.dims: kz = kz_func(self.kinetic_energy, self.inner_potential, kx, ky) @@ -539,7 +539,7 @@ def _get_transformed_coords(self) -> dict[str, xr.DataArray]: else: return {"kx": kx, "ky": ky} - def estimate_bounds(self) -> dict[str, tuple[float, float]]: + def estimate_bounds(self) -> dict[Literal["kx", "ky", "kz"], tuple[float, float]]: """ Estimates the bounds of the data in momentum space based on the available parameters. @@ -605,7 +605,7 @@ def estimate_resolution( else: raise ValueError(f"`{axis}` is not a valid momentum axis.") - if from_numpoints: + if from_numpoints and (lims is not None): return float((lims[1] - lims[0]) / len(self._obj[dim])) elif axis == "kz": return self.best_kz_resolution @@ -637,7 +637,7 @@ def _inverse_broadcast(self, kx, ky, kz=None) -> dict[str, xr.DataArray]: if self.has_eV: out_dict["eV"] = self.binding_energy - if kz is not None: + if kzval is not None: out_dict["hv"] = ( rel_kzconv * (kxval**2 + kyval**2 + kzval**2) - self.inner_potential @@ -645,7 +645,16 @@ def _inverse_broadcast(self, kx, ky, kz=None) -> dict[str, xr.DataArray]: - self.binding_energy ) - return dict(zip(out_dict.keys(), xr.broadcast(*out_dict.values()))) + return cast( + dict[str, xr.DataArray], + dict( + zip( + cast(list[str], out_dict.keys()), + xr.broadcast(*out_dict.values()), + strict=True, + ) + ), + ) @only_angles def convert_coords(self) -> xr.DataArray: @@ -662,7 +671,7 @@ def convert_coords(self) -> xr.DataArray: return self._obj.assign_coords(self._get_transformed_coords()) @only_angles - def _get_coord_for_conversion(self, name: str) -> xr.DataArray: + def _get_coord_for_conversion(self, name: Hashable) -> xr.DataArray: """ Get the coordinte array for given dimension name. This just ensures that the energy coordinates are given as binding energy. @@ -779,6 +788,12 @@ def convert( print(f"Data spans {lims[1] - lims[0]:.3f} Å⁻¹ of {k}") momentum_coords[k] = np.array([(lims[0] + lims[1]) / 2]) + for k, v in coords.items(): + if k in self.momentum_axes: + momentum_coords[k] = v + else: + raise ValueError(f"Dimension `{k}` is not a momentum axis") + if not silent: print("Calculating destination coordinates") @@ -803,13 +818,13 @@ def convert( dim_mapping: dict[str, str] = {} for d in coords_for_transform.dims: if d == self.slit_axis: - dim_mapping["alpha"] = d + dim_mapping["alpha"] = str(d) elif d == self.other_axis: - dim_mapping["beta"] = d + dim_mapping["beta"] = str(d) elif d == "kz": - dim_mapping["hv"] = d + dim_mapping["hv"] = str(d) else: - dim_mapping[d] = d + dim_mapping[str(d)] = str(d) # Delete keys not in the input data, e.g. "beta" for cuts for k in list(dim_mapping.keys()): @@ -828,8 +843,10 @@ def _wrap_interpn(arr, *args): return interpn(points, arr, xi, bounds_error=False).squeeze() input_core_dims = [input_dims] - input_core_dims.extend([[d] for d in input_dims]) - input_core_dims.extend([target_dict[d].dims for d in input_dims]) + input_core_dims.extend([(d,) for d in input_dims]) + input_core_dims.extend( + [cast(tuple[str, ...], target_dict[d].dims) for d in input_dims] + ) out = xr.apply_ufunc( _wrap_interpn, @@ -853,7 +870,7 @@ def _wrap_interpn(arr, *args): return out - def interactive(self, **kwargs) -> ktool: + def interactive(self, **kwargs) -> KspaceTool: """Open the interactive momentum space conversion tool.""" if self._obj.ndim < 3: raise ValueError("Interactive tool requires three-dimensional data.") diff --git a/src/erlab/accessors/utils.py b/src/erlab/accessors/utils.py index af4e7169..607f211b 100644 --- a/src/erlab/accessors/utils.py +++ b/src/erlab/accessors/utils.py @@ -21,10 +21,17 @@ _THIS_ARRAY: str = "" -class ERLabAccessor: +class ERLabDataArrayAccessor: """Base class for accessors.""" - def __init__(self, xarray_obj: xr.DataArray | xr.Dataset): + def __init__(self, xarray_obj: xr.DataArray): + self._obj = xarray_obj + + +class ERLabDatasetAccessor: + """Base class for accessors.""" + + def __init__(self, xarray_obj: xr.Dataset): self._obj = xarray_obj @@ -52,7 +59,7 @@ def either_dict_or_kwargs( @xr.register_dataarray_accessor("qplot") -class PlotAccessor(ERLabAccessor): +class PlotAccessor(ERLabDataArrayAccessor): """`xarray.DataArray.qplot` accessor for plotting data.""" def __call__(self, *args, **kwargs): @@ -83,10 +90,10 @@ def __call__(self, *args, **kwargs): @xr.register_dataarray_accessor("qshow") -class ImageToolAccessor(ERLabAccessor): +class ImageToolAccessor(ERLabDataArrayAccessor): """`xarray.DataArray.qshow` accessor for interactive visualization.""" - def __call__(self, *args, **kwargs) -> ImageTool: + def __call__(self, *args, **kwargs) -> ImageTool | list[ImageTool] | None: if len(self._obj.dims) >= 2: return itool(self._obj, *args, **kwargs) else: @@ -94,7 +101,7 @@ def __call__(self, *args, **kwargs) -> ImageTool: @xr.register_dataarray_accessor("qsel") -class SelectionAccessor(ERLabAccessor): +class SelectionAccessor(ERLabDataArrayAccessor): """ `xarray.DataArray.qsel` accessor for conveniently selecting and averaging data. @@ -102,7 +109,7 @@ class SelectionAccessor(ERLabAccessor): def __call__( self, - indexers: dict[str, float | slice] | None = None, + indexers: Mapping[Hashable, float | slice] | None = None, *, verbose: bool = False, **indexers_kwargs, @@ -151,28 +158,39 @@ def __call__( indexers = either_dict_or_kwargs(indexers, indexers_kwargs, "qsel") # Bin widths for each dimension, zero if width not specified - bin_widths: dict[str, float] = {} + bin_widths: dict[Hashable, float] = {} for dim in indexers: - if not dim.endswith("_width"): - bin_widths[dim] = indexers.get(f"{dim}_width", 0.0) + if not str(dim).endswith("_width"): + width = indexers.get(f"{dim}_width", 0.0) + if isinstance(width, slice): + raise ValueError( + f"Slice not allowed for width of dimension `{dim}`" + ) + else: + bin_widths[dim] = float(width) if dim not in self._obj.dims: raise ValueError(f"Dimension `{dim}` not found in data.") - scalars: dict[str, float] = {} - slices: dict[str, slice] = {} - avg_dims: list[str] = [] + scalars: dict[Hashable, float] = {} + slices: dict[Hashable, slice] = {} + avg_dims: list[Hashable] = [] for dim, width in bin_widths.items(): + value = indexers[dim] + if width == 0.0: - if isinstance(indexers[dim], slice): - slices[dim] = indexers[dim] + if isinstance(value, slice): + slices[dim] = value else: - scalars[dim] = float(indexers[dim]) + scalars[dim] = float(value) else: - slices[dim] = slice( - indexers[dim] - width / 2, indexers[dim] + width / 2 - ) + if isinstance(value, slice): + raise ValueError( + f"Slice not allowed for value of dimension `{dim}` " + "with width specified" + ) + slices[dim] = slice(value - width / 2, value + width / 2) avg_dims.append(dim) if len(scalars) >= 1: @@ -182,12 +200,14 @@ def __call__( f"Selected value {v} for `{k}` is outside coordinate bounds", stacklevel=2, ) - out = self._obj.sel(**scalars, method="nearest") + out = self._obj.sel( + {str(k): v for k, v in scalars.items()}, method="nearest" + ) else: out = self._obj if len(slices) >= 1: - out = out.sel(**slices) + out = out.sel(slices) lost_coords = {k: out[k].mean() for k in avg_dims} out = out.mean(dim=avg_dims, keep_attrs=True) diff --git a/src/erlab/analysis/correlation.py b/src/erlab/analysis/correlation.py index c3c65ae5..30f46be9 100644 --- a/src/erlab/analysis/correlation.py +++ b/src/erlab/analysis/correlation.py @@ -91,7 +91,7 @@ def acf2(arr, mode: str = "full", method: str = "fft"): acf, { d: autocorrelation_lags(n, mode) * s - for s, n, d in zip(steps, arr.shape, out.dims) + for s, n, d in zip(steps, arr.shape, out.dims, strict=True) }, attrs=out.attrs, ) @@ -114,14 +114,14 @@ def acf2stack(arr, stack_dims=("eV",), mode: str = "full", method: str = "fft"): out_list = joblib.Parallel(n_jobs=-1, pre_dispatch="3 * n_jobs")( joblib.delayed(nanacf)( - np.squeeze(arr.isel(dict(zip(stack_dims, vals))).values), + np.squeeze(arr.isel(dict(zip(stack_dims, vals, strict=True))).values), mode, method, ) for vals in itertools.product(*stack_iter) ) acf_dims = tuple(filter(lambda d: d not in stack_dims, arr.dims)) - acf_sizes = dict(zip(acf_dims, out_list[0].shape)) + acf_sizes = dict(zip(acf_dims, out_list[0].shape, strict=True)) acf_steps = tuple(arr[d].values[1] - arr[d].values[0] for d in acf_dims) out_sizes = stack_sizes | acf_sizes @@ -137,12 +137,14 @@ def acf2stack(arr, stack_dims=("eV",), mode: str = "full", method: str = "fft"): out = out.assign_coords({d: arr[d] for d in stack_dims}) for i, vals in enumerate(itertools.product(*stack_iter)): - out.loc[{s: arr[s][v] for s, v in zip(stack_dims, vals)}] = out_list[i] + out.loc[{s: arr[s][v] for s, v in zip(stack_dims, vals, strict=True)}] = ( + out_list[i] + ) out = out.assign_coords( { d: autocorrelation_lags(len(arr[d]), mode) * s - for s, d in zip(acf_steps, acf_dims) + for s, d in zip(acf_steps, acf_dims, strict=True) } ) if all(i in out.dims for i in ["kx", "ky"]): diff --git a/src/erlab/analysis/fit/functions/dynamic.py b/src/erlab/analysis/fit/functions/dynamic.py index c374f2eb..909b0fe8 100644 --- a/src/erlab/analysis/fit/functions/dynamic.py +++ b/src/erlab/analysis/fit/functions/dynamic.py @@ -13,7 +13,8 @@ ] import functools import inspect -from collections.abc import Callable +from collections.abc import Callable, Sequence +from typing import Any, TypedDict, no_type_check, ClassVar import numpy as np import numpy.typing as npt @@ -30,7 +31,12 @@ from erlab.constants import kb_eV -def get_args_kwargs(func) -> tuple[list[str], dict[str, object]]: +class PeakArgs(TypedDict): + args: list[str] + kwargs: dict[str, Any] + + +def get_args_kwargs(func: Callable) -> tuple[list[str], dict[str, Any]]: """Get all argument names and default values from a function signature. Parameters @@ -72,6 +78,11 @@ def get_args_kwargs(func) -> tuple[list[str], dict[str, object]]: return args, args_default +def get_args_kwargs_dict(func: Callable) -> PeakArgs: + args, kwargs = get_args_kwargs(func) + return {"args": args, "kwargs": kwargs} + + class DynamicFunction: """Base class for dynamic functions. @@ -81,7 +92,7 @@ class DynamicFunction: @property def __name__(self) -> str: - return self.__class__.__name__ + return str(self.__class__.__name__) @property def argnames(self) -> list[str]: @@ -91,7 +102,8 @@ def argnames(self) -> list[str]: def kwargs(self) -> dict[str, int | float]: return {} - def __call__(self, x: npt.NDArray[np.float64], **params) -> npt.NDArray[np.float64]: + @no_type_check + def __call__(self, **kwargs): raise NotImplementedError("Must be overloaded in child classes") @@ -149,7 +161,7 @@ class MultiPeakFunction(DynamicFunction): """ - PEAK_SHAPES: dict[Callable, list[str]] = { + PEAK_SHAPES: ClassVar[dict[Callable, list[str]]] = { lorentzian_wh: ["lorentzian", "lor", "l"], gaussian_wh: ["gaussian", "gauss", "g"], } @@ -180,20 +192,20 @@ def __init__( self._peak_shapes = peak_shapes - self._peak_funcs = [None] * self.npeaks - for i, name in enumerate(self._peak_shapes): + self._peak_funcs: list[Callable] = [] + for name in self._peak_shapes: for fcn, aliases in self.PEAK_SHAPES.items(): if name in aliases: - self._peak_funcs[i] = fcn + self._peak_funcs.append(fcn) - if None in self._peak_funcs: + if len(self._peak_funcs) != self.npeaks: raise ValueError("Invalid peak name") @functools.cached_property - def peak_all_args(self) -> dict[Callable, dict[str, list | dict]]: - res = {} + def peak_all_args(self) -> dict[Callable, PeakArgs]: + res: dict[Callable, PeakArgs] = {} for func in self.PEAK_SHAPES: - res[func] = dict(zip(("args", "kwargs"), get_args_kwargs(func))) + res[func] = get_args_kwargs_dict(func) return res @functools.cached_property @@ -201,12 +213,12 @@ def peak_argnames(self) -> dict[Callable, list[str]]: res = {} for func in self.PEAK_SHAPES: res[func] = self.peak_all_args[func]["args"][1:] + list( - self.peak_all_args[func]["kwargs"].keys() + dict(self.peak_all_args[func]["kwargs"]).keys() ) return res @property - def peak_funcs(self) -> list[Callable]: + def peak_funcs(self) -> Sequence[Callable]: return self._peak_funcs @property @@ -232,7 +244,7 @@ def kwargs(self): kws += [("resolution", 0.02)] for i, func in enumerate(self.peak_funcs): - for arg, val in self.peak_all_args[func]["kwargs"].items(): + for arg, val in dict(self.peak_all_args[func]["kwargs"]).items(): kws.append((f"p{i}_{arg}", val)) return kws @@ -252,7 +264,7 @@ def amplitude_expr(self, index: int, prefix: str) -> str | None: else: return None - def eval_peak(self, index: int, x: npt.NDArray[np.float64], **params: dict): + def eval_peak(self, index: int, x, **params): return self.peak_funcs[index]( x, **{ @@ -262,12 +274,10 @@ def eval_peak(self, index: int, x: npt.NDArray[np.float64], **params: dict): }, ) - def eval_bkg(self, x: npt.NDArray[np.float64], **params: dict): + def eval_bkg(self, x, **params): return params["lin_bkg"] * x + params["const_bkg"] - def pre_call( - self, x: npt.NDArray[np.float64], **params: dict - ) -> npt.NDArray[np.float64]: + def pre_call(self, x, **params): x = np.asarray(x).copy() y = np.zeros_like(x) @@ -284,9 +294,7 @@ def pre_call( return y - def __call__( - self, x: npt.NDArray[np.float64], **params: dict - ) -> npt.NDArray[np.float64]: + def __call__(self, x, **params): if isinstance(x, xr.DataArray): return x * 0.0 + self.__call__(x.values, **params) @@ -319,7 +327,7 @@ def kwargs(self): ("resolution", 0.02), ] - def pre_call(self, eV, alpha, **params: dict): + def pre_call(self, eV, alpha, **params): center = self.poly( np.asarray(alpha), *[params.pop(f"c{i}") for i in range(self.poly.degree + 1)], @@ -328,15 +336,20 @@ def pre_call(self, eV, alpha, **params: dict): 1 + np.exp((1.0 * eV - center) / max(TINY, params["temp"] * kb_eV)) ) + params["offset"] - def __call__(self, eV, alpha, **params: dict): + def __call__( + self, + eV: npt.NDArray[np.float64] | xr.DataArray, + alpha: npt.NDArray[np.float64] | xr.DataArray, + **params, + ): if isinstance(eV, xr.DataArray) and isinstance(alpha, xr.DataArray): out = eV * alpha * 0.0 return out + self.__call__(eV.values, alpha.values, **params).reshape( out.shape ) - if isinstance("eV", xr.DataArray): + if isinstance(eV, xr.DataArray): eV = eV.values - if isinstance("alpha", xr.DataArray): + if isinstance(alpha, xr.DataArray): alpha = alpha.values if "resolution" not in params: raise TypeError("Missing parameter `resolution` required for convolution") diff --git a/src/erlab/analysis/fit/functions/general.py b/src/erlab/analysis/fit/functions/general.py index ba13f087..7a6b2335 100644 --- a/src/erlab/analysis/fit/functions/general.py +++ b/src/erlab/analysis/fit/functions/general.py @@ -132,13 +132,17 @@ def do_convolve( def do_convolve_2d( x: npt.NDArray[np.float64], - y: npt.NDArray[np.float64], + y: npt.NDArray[np.float64] | float, func: Callable, resolution: float, pad: int = 5, **kwargs, ) -> npt.NDArray[np.float64]: idx_x = None + + if not np.iterable(y): + y = np.asarray([y]) + try: # check if x is a meshgrid shape_x, idx_x, x = _infer_meshgrid_shape(np.ascontiguousarray(x)) @@ -153,9 +157,6 @@ def do_convolve_2d( np.asarray(np.squeeze(x), dtype=np.float64), resolution, pad=pad ) - if not np.iterable(y): - y = [y] - convolved = np.vstack( [ np.convolve(func(xn, yi, **kwargs), g, mode="valid") diff --git a/src/erlab/analysis/fit/minuit.py b/src/erlab/analysis/fit/minuit.py index c3d986e5..3cdae183 100644 --- a/src/erlab/analysis/fit/minuit.py +++ b/src/erlab/analysis/fit/minuit.py @@ -1,13 +1,18 @@ from __future__ import annotations +import importlib from collections.abc import Iterable, Sequence from typing import TYPE_CHECKING +if not importlib.util.find_spec("iminuit"): + raise ImportError("`erlab.analysis.fit.minuit` requires `iminuit` to be installed.") + import iminuit.cost import iminuit.util import matplotlib.pyplot as plt import numpy as np import numpy.typing as npt +import xarray from iminuit.util import _detect_log_spacing, _smart_sampling import erlab.plotting.general @@ -27,7 +32,7 @@ def visualize( if self._ndim > 1: raise ValueError("visualize is not implemented for multi-dimensional data") - plt.grid(visible="both") + plt.grid(visible=True, axis="both") x, y, ye = self._masked.T plt.errorbar( x, y, ye, fmt="o", lw=0.75, ms=3, mfc="w", zorder=2, c="0.4", capsize=0 @@ -43,7 +48,10 @@ def visualize( ym = self.model(xm, *args) else: xm, ym = _smart_sampling( - lambda x: self.model(x, *args), x[0], x[-1], start=len(x) + lambda x: self.model(x, *args), + x[0], + x[-1], + start=len(x), ) plt.plot(xm, ym, "r-", lw=1, zorder=3) return (x, y, ye), (xm, ym) @@ -100,17 +108,19 @@ class Minuit(iminuit.Minuit): def from_lmfit( cls, model: lmfit.Model, - data: npt.ArrayLike, - ivars: npt.ArrayLike | Sequence[npt.ArrayLike], - yerr: float | npt.ArrayLike | None = None, + data: npt.NDArray | xarray.DataArray, + ivars: npt.NDArray + | xarray.DataArray + | Sequence[npt.NDArray | xarray.DataArray], + yerr: float | npt.NDArray | None = None, return_cost: bool = False, **kwargs, ) -> Minuit | tuple[LeastSq, Minuit]: if len(model.independent_vars) == 1: - if len(ivars) != 1: + if isinstance(ivars, np.ndarray | xarray.DataArray): ivars = [ivars] - x = [np.asarray(a) for a in ivars] + x: npt.NDArray | list[npt.NDArray] = [np.asarray(a) for a in ivars] if len(x) != len(model.independent_vars): raise ValueError("Number of independent variables does not match model.") @@ -173,12 +183,16 @@ def from_lmfit( if len(model.independent_vars) == 1: def _temp_func(x, *fargs): - return model.func(x, **dict(zip(model._param_root_names, fargs))) + return model.func( + x, **dict(zip(model._param_root_names, fargs, strict=True)) + ) else: def _temp_func(x, *fargs): - return model.func(*x, **dict(zip(model._param_root_names, fargs))) + return model.func( + *x, **dict(zip(model._param_root_names, fargs, strict=True)) + ) c = LeastSq(x, data, yerr, _temp_func) m = cls(c, name=param_names, **values) diff --git a/src/erlab/analysis/fit/models.py b/src/erlab/analysis/fit/models.py index e251bb55..85e6d654 100644 --- a/src/erlab/analysis/fit/models.py +++ b/src/erlab/analysis/fit/models.py @@ -115,8 +115,11 @@ class FermiEdgeModel(lmfit.Model): """ Fermi-dirac function with linear background above and below the fermi level, convolved with a gaussian kernel. + """ + __doc__ = __doc__ + lmfit.models.COMMON_INIT_DOC + @staticmethod def LinearBroadFermiDirac( x, @@ -165,7 +168,6 @@ def guess(self, data, x, **kwargs): return lmfit.models.update_param_vals(pars, self.prefix, **kwargs) - __init__.doc = lmfit.models.COMMON_INIT_DOC guess.__doc__ = COMMON_GUESS_DOC @@ -199,7 +201,7 @@ def guess(self, data, x, **kwargs): return lmfit.models.update_param_vals(pars, self.prefix, **kwargs) - __init__.doc = lmfit.models.COMMON_INIT_DOC + __doc__ = lmfit.models.COMMON_INIT_DOC guess.__doc__ = COMMON_GUESS_DOC @@ -220,7 +222,7 @@ def guess(self, data, x=None, **kwargs): pars[f"{self.prefix}c{i}"].set(value=coef) return lmfit.models.update_param_vals(pars, self.prefix, **kwargs) - __init__.doc = lmfit.models.COMMON_INIT_DOC + __doc__ = lmfit.models.COMMON_INIT_DOC guess.__doc__ = COMMON_GUESS_DOC @@ -326,7 +328,7 @@ class FermiEdge2dModel(lmfit.Model): :math:`c` convolved with a gaussian, where :math:`\omega` is the binding energy and :math:`\alpha` is the detector angle. - """ + """ + lmfit.models.COMMON_INIT_DOC.replace("['x']", "['eV', 'alpha']") def __init__( self, @@ -381,7 +383,6 @@ def fit(self, data, *args, **kwargs): # Ensure flat fit return super().fit(data.ravel(), *args, **kwargs) - __init__.__doc__ = lmfit.models.COMMON_INIT_DOC.replace("['x']", "['eV', 'alpha']") guess.__doc__ = COMMON_GUESS_DOC.replace("x : ", "eV, alpha : ") @@ -392,7 +393,7 @@ def __init__(self, **kwargs): self.set_param_hint("tc", min=0.0) __doc__ = bcs_gap.__doc__ - __init__.doc = lmfit.models.COMMON_INIT_DOC + __init__.__doc__ = lmfit.models.COMMON_INIT_DOC class DynesModel(lmfit.Model): @@ -402,4 +403,4 @@ def __init__(self, **kwargs): self.set_param_hint("delta", min=0.0) __doc__ = dynes.__doc__ - __init__.doc = lmfit.models.COMMON_INIT_DOC + __init__.__doc__ = lmfit.models.COMMON_INIT_DOC diff --git a/src/erlab/analysis/fit/spline.py b/src/erlab/analysis/fit/spline.py index f2e759c0..47e449e7 100644 --- a/src/erlab/analysis/fit/spline.py +++ b/src/erlab/analysis/fit/spline.py @@ -4,8 +4,7 @@ import csaps except ImportError as e: raise ImportError( - "The `csaps` package is required for this module. " - "Please install it using `pip install csaps`." + "`erlab.analysis.fit.spline` requires `csaps` to be installed." ) from e diff --git a/src/erlab/analysis/gold.py b/src/erlab/analysis/gold.py index 2f878161..fb2a5bb3 100644 --- a/src/erlab/analysis/gold.py +++ b/src/erlab/analysis/gold.py @@ -10,9 +10,10 @@ "spline_from_edge", ] -from collections.abc import Callable, Sequence +from collections.abc import Callable import joblib +import lmfit import lmfit.model import matplotlib import matplotlib.figure @@ -142,7 +143,7 @@ def correct_with_edge( def edge( - gold: xr.DataArray | xr.Dataset, + gold: xr.DataArray, angle_range: tuple[float, float], eV_range: tuple[float, float], bin_size: tuple[int, int] = (1, 1), @@ -158,7 +159,7 @@ def edge( parallel_obj: joblib.Parallel | None = None, return_full: bool = False, **kwargs, -) -> tuple[npt.NDArray, npt.NDArray] | xr.Dataset: +) -> tuple[xr.DataArray, xr.DataArray] | list[lmfit.model.ModelResult]: """ Fit a Fermi edge to the given gold data. @@ -211,9 +212,10 @@ def edge( `True`. """ + if fast: params = lmfit.create_params() - model_cls = StepEdgeModel + model_cls: lmfit.Model = StepEdgeModel else: if temp is None: temp = gold.attrs["temp_sample"] @@ -230,12 +232,12 @@ def edge( if any(b != 1 for b in bin_size): gold_binned = gold.coarsen(alpha=bin_size[0], eV=bin_size[1], boundary="trim") - gold = gold_binned.mean() + gold = gold_binned.mean() # type: ignore[attr-defined] gold_sel = gold.sel(alpha=slice(*angle_range), eV=slice(*eV_range)) # Assuming Poisson noise, the weights are the square root of the counts. - weights = 1 / np.sqrt(gold_sel.sum("eV").values) + weights = 1 / np.sqrt(np.asarray(gold_sel.sum("eV").values)) n_fits = len(gold_sel.alpha) @@ -273,7 +275,7 @@ def _fit(data, w): tqdm_kw = {"desc": "Fitting", "total": n_fits, "disable": not progress} if parallel_obj.return_generator: - fitresults = tqdm.auto.tqdm( + fitresults = tqdm.auto.tqdm( # type: ignore[call-overload] parallel_obj( joblib.delayed(_fit)(gold_sel.isel(alpha=i), weights[i]) for i in range(n_fits) @@ -296,7 +298,7 @@ def _fit(data, w): if return_full: return list(fitresults) - xval = [] + xval: list[npt.NDArray] = [] res_vals = [] for i, r in enumerate(fitresults): @@ -310,13 +312,10 @@ def _fit(data, w): xval.append(gold_sel.alpha.values[i]) res_vals.append([center_ufloat.nominal_value, center_ufloat.std_dev]) - xval = np.asarray(xval) + coords = {"alpha": np.asarray(xval)} yval, yerr = np.asarray(res_vals).T - return ( - xr.DataArray(yval, coords={"alpha": xval}), - xr.DataArray(yerr, coords={"alpha": xval}), - ) + return xr.DataArray(yval, coords=coords), xr.DataArray(yerr, coords=coords) def poly_from_edge( @@ -336,7 +335,7 @@ def poly_from_edge( def spline_from_edge( - center, weights: Sequence[float] | None = None, lam: float | None = None + center, weights: npt.ArrayLike | None = None, lam: float | None = None ) -> scipy.interpolate.BSpline: spl = scipy.interpolate.make_smoothing_spline( center.alpha.values, @@ -448,7 +447,7 @@ def _plot_gold_fit(fig, gold, angle_range, eV_range, center_arr, center_stderr, def poly( - gold: xr.DataArray | xr.Dataset, + gold: xr.DataArray, angle_range: tuple[float, float], eV_range: tuple[float, float], bin_size: tuple[int, int] = (1, 1), @@ -501,7 +500,7 @@ def poly( def spline( - gold: xr.DataArray | xr.Dataset, + gold: xr.DataArray, angle_range: tuple[float, float], eV_range: tuple[float, float], bin_size: tuple[int, int] = (1, 1), @@ -543,7 +542,7 @@ def spline( def resolution( - gold: xr.DataArray | xr.Dataset, + gold: xr.DataArray, angle_range: tuple[float, float], eV_range_edge: tuple[float, float], eV_range_fit: tuple[float, float] | None = None, diff --git a/src/erlab/analysis/image.py b/src/erlab/analysis/image.py index c3d09bf2..ae8dbe79 100644 --- a/src/erlab/analysis/image.py +++ b/src/erlab/analysis/image.py @@ -7,7 +7,7 @@ unlike the scipy default of 'reflect'. """ -from collections.abc import Sequence +from collections.abc import Collection, Mapping, Sequence, Sized, Hashable import numpy as np import numpy.typing as npt @@ -19,13 +19,13 @@ def gaussian_filter( darr: xr.DataArray, - sigma: float | dict[str, float] | Sequence[float], - order: int | Sequence[int] | dict[str, int] = 0, - mode: str | Sequence[str] | dict[str, str] = "nearest", + sigma: float | Collection[float] | Mapping[Hashable, float], + order: int | Sequence[int] | Mapping[Hashable, int] = 0, + mode: str | Sequence[str] | Mapping[Hashable, str] = "nearest", cval: float = 0.0, truncate: float = 4.0, *, - radius: None | float | Sequence[float] | dict[str, float] = None, + radius: None | float | Collection[float] | Mapping[Hashable, float] = None, ) -> xr.DataArray: """Coordinate-aware wrapper around `scipy.ndimage.gaussian_filter`. @@ -99,48 +99,58 @@ def gaussian_filter( Dimensions without coordinates: x, y """ - if np.isscalar(sigma): - sigma = dict.fromkeys(darr.dims, sigma) - elif not isinstance(sigma, dict): - sigma = dict(zip(darr.dims, sigma)) + if isinstance(sigma, Mapping): + sigma_dict = dict(sigma) + elif np.isscalar(sigma): + sigma_dict = dict.fromkeys(darr.dims, sigma) + elif isinstance(sigma, Collection): + sigma_dict = dict(zip(darr.dims, sigma, strict=True)) + else: + raise TypeError("`sigma` must be a scalar, sequence, or mapping") # Get the axis indices to apply the filter - axes = tuple(darr.get_axis_num(d) for d in sigma.keys()) + axes = tuple(darr.get_axis_num(d) for d in sigma_dict.keys()) # Convert arguments to tuples acceptable by scipy - if isinstance(order, dict): - order = tuple(order.get(d, 0) for d in sigma.keys()) - if isinstance(mode, dict): - mode = tuple(mode[d] for d in sigma.keys()) - if radius is not None: - if len(radius) != len(sigma): - raise ValueError("`radius` does not match dimensions of `sigma`") + if isinstance(order, Mapping): + order = tuple(order.get(str(d), 0) for d in sigma_dict.keys()) + if isinstance(mode, Mapping): + mode = tuple(mode[str(d)] for d in sigma_dict.keys()) - if np.isscalar(radius): - radius = dict.fromkeys(sigma.keys(), radius) - elif not isinstance(radius, dict): - radius = dict(zip(sigma.keys(), radius)) + if radius is not None: + if isinstance(radius, Mapping): + radius_dict = dict(radius) + elif isinstance(radius, Sized): + if len(radius) != len(sigma_dict): + raise ValueError("`radius` does not match dimensions of `sigma`") + radius_dict = dict(zip(sigma_dict.keys(), radius, strict=True)) + elif np.isscalar(radius): + radius_dict = dict.fromkeys(sigma_dict.keys(), radius) + else: + raise TypeError("`radius` must be a scalar, sequence, or mapping") # Calculate radius in pixels - radius: tuple[int, ...] = tuple( + radius_pix: tuple[int, ...] | None = tuple( round(r / (darr[d].values[1] - darr[d].values[0])) - for d, r in radius.items() + for d, r in radius_dict.items() ) + else: + radius_pix = None # Calculate sigma in pixels - sigma: tuple[float, ...] = tuple( - val / (darr[d].values[1] - darr[d].values[0]) for d, val in sigma.items() + sigma_pix: tuple[float, ...] = tuple( + val / (darr[d].values[1] - darr[d].values[0]) for d, val in sigma_dict.items() ) return darr.copy( data=scipy.ndimage.gaussian_filter( darr.values, - sigma=sigma, + sigma=sigma_pix, order=order, mode=mode, cval=cval, truncate=truncate, - radius=radius, + radius=radius_pix, axes=axes, ) ) @@ -148,8 +158,8 @@ def gaussian_filter( def gaussian_laplace( darr: xr.DataArray, - sigma: float | dict[str, float] | Sequence[float], - mode: str | Sequence[str] | dict[str, str] = "nearest", + sigma: float | Collection[float] | Mapping[str, float], + mode: str | Sequence[str] | Mapping[str, str] = "nearest", cval: float = 0.0, **kwargs, ) -> xr.DataArray: @@ -195,28 +205,35 @@ def gaussian_laplace( :func:`scipy.ndimage.gaussian_laplace` : The underlying function used to apply the filter. """ - if np.isscalar(sigma): - sigma = dict.fromkeys(darr.dims, sigma) - elif not isinstance(sigma, dict): - sigma = dict(zip(darr.dims, sigma)) - if len(sigma) != darr.ndim: + if isinstance(sigma, Mapping): + sigma_dict = dict(sigma) + elif np.isscalar(sigma): + sigma_dict = dict.fromkeys(darr.dims, sigma) + elif isinstance(sigma, Collection): + sigma_dict = dict(zip(darr.dims, sigma, strict=True)) + else: + raise TypeError("`sigma` must be a scalar, sequence, or mapping") + + if len(sigma_dict) != darr.ndim: + required_dims = set(darr.dims) - set(sigma_dict.keys()) raise ValueError( - "`sigma` must be provided for every dimension of the DataArray" + "`sigma` missing for the following dimension" + f"{'' if len(required_dims) == 1 else 's'}: {required_dims}" ) # Convert mode to tuple acceptable by scipy if isinstance(mode, dict): - mode = tuple(mode[d] for d in sigma.keys()) + mode = tuple(mode[d] for d in sigma_dict.keys()) # Calculate sigma in pixels - sigma: tuple[float, ...] = tuple( - val / (darr[d].values[1] - darr[d].values[0]) for d, val in sigma.items() + sigma_pix: tuple[float, ...] = tuple( + val / (darr[d].values[1] - darr[d].values[0]) for d, val in sigma_dict.items() ) return darr.copy( data=scipy.ndimage.gaussian_laplace( - darr.values, sigma=sigma, mode=mode, cval=cval, **kwargs + darr.values, sigma=sigma_pix, mode=mode, cval=cval, **kwargs ) ) diff --git a/src/erlab/analysis/interpolate.py b/src/erlab/analysis/interpolate.py index c6b08b9a..991aa43f 100644 --- a/src/erlab/analysis/interpolate.py +++ b/src/erlab/analysis/interpolate.py @@ -350,5 +350,5 @@ def _get_interpolator_nd_fast(method, **kwargs): return _get_interpolator_nd_original(method, **kwargs) -xarray.core.missing._get_interpolator = _get_interpolator_fast +xarray.core.missing._get_interpolator = _get_interpolator_fast # type: ignore[assignment] xarray.core.missing._get_interpolator_nd = _get_interpolator_nd_fast diff --git a/src/erlab/analysis/kspace.py b/src/erlab/analysis/kspace.py index 8f90eb1f..ef875ae6 100644 --- a/src/erlab/analysis/kspace.py +++ b/src/erlab/analysis/kspace.py @@ -12,6 +12,7 @@ import numpy as np import numpy.typing as npt +import xarray import erlab.constants import erlab.io @@ -64,7 +65,7 @@ def kz_func(kinetic_energy, inner_potential, kx, ky): def get_kconv_func( - kinetic_energy: float | npt.NDArray, + kinetic_energy: float | npt.NDArray | xarray.DataArray, configuration: AxesConfiguration, angle_params: dict[str, float], ) -> tuple[Callable, Callable]: @@ -122,7 +123,7 @@ def get_kconv_func( match configuration: case AxesConfiguration.Type1: - func = _kconv_func_type1 + func: Callable = _kconv_func_type1 case AxesConfiguration.Type2: func = _kconv_func_type2 case AxesConfiguration.Type1DA: @@ -135,13 +136,7 @@ def get_kconv_func( return func(k_tot, **angle_params) -def _kconv_func_type1( - k_tot: float | npt.NDArray, - delta: float = 0.0, - xi: float = 0.0, - xi0: float = 0.0, - beta0: float = 0.0, -): +def _kconv_func_type1(k_tot, delta=0.0, xi=0.0, xi0=0.0, beta0=0.0): cd, sd = np.cos(np.deg2rad(delta)), np.sin(np.deg2rad(delta)) # δ cx, sx = np.cos(np.deg2rad(xi - xi0)), np.sin(np.deg2rad(xi - xi0)) # ξ - ξ0 @@ -179,13 +174,7 @@ def _inverse_func(kx, ky, kz=None): return _forward_func, _inverse_func -def _kconv_func_type2( - k_tot: float | npt.NDArray, - delta: float = 0.0, - xi: float = 0.0, - xi0: float = 0.0, - beta0: float = 0.0, -): +def _kconv_func_type2(k_tot, delta=0.0, xi=0.0, xi0=0.0, beta0=0.0): cd, sd = np.cos(np.deg2rad(delta)), np.sin(np.deg2rad(delta)) # δ cx, sx = np.cos(np.deg2rad(xi - xi0)), np.sin(np.deg2rad(xi - xi0)) # ξ - ξ0 @@ -223,14 +212,7 @@ def _inverse_func(kx, ky, kz=None): return _forward_func, _inverse_func -def _kconv_func_type1_da( - k_tot: float | npt.NDArray, - delta: float = 0.0, - chi: float = 0.0, - chi0: float = 0.0, - xi: float = 0.0, - xi0: float = 0.0, -): +def _kconv_func_type1_da(k_tot, delta=0.0, chi=0.0, chi0=0.0, xi=0.0, xi0=0.0): _fwd_2, _inv_2 = _kconv_func_type2_da(k_tot, delta, chi, chi0, xi, xi0) def _forward_func(alpha, beta): @@ -243,14 +225,7 @@ def _inverse_func(kx, ky, kz=None): return _forward_func, _inverse_func -def _kconv_func_type2_da( - k_tot: float | npt.NDArray, - delta: float = 0.0, - chi: float = 0.0, - chi0: float = 0.0, - xi: float = 0.0, - xi0: float = 0.0, -): +def _kconv_func_type2_da(k_tot, delta=0.0, chi=0.0, chi0=0.0, xi=0.0, xi0=0.0): cd, sd = np.cos(np.deg2rad(delta)), np.sin(np.deg2rad(delta)) # δ, azimuth cx, sx = np.cos(np.deg2rad(xi - xi0)), np.sin(np.deg2rad(xi - xi0)) # ξ cc, sc = np.cos(np.deg2rad(chi - chi0)), np.sin(np.deg2rad(chi - chi0)) # χ @@ -300,7 +275,7 @@ def _inverse_func(kx, ky, kz=None): k_sq = kx**2 + ky**2 + kz**2 k = np.sqrt(k_sq) - kperp = _kperp_func(k_sq, kx, ky) # sqrt(k² − k_x² − k_y²) + kperp = _kperp_func(k_sq, kx, ky) # sqrt(k² - k_x² - k_y²) proj1 = t11 * kx + t12 * ky + t13 * kperp proj2 = t21 * kx + t22 * ky + t23 * kperp diff --git a/src/erlab/analysis/mask/polygon.py b/src/erlab/analysis/mask/polygon.py index e4574872..42e59843 100644 --- a/src/erlab/analysis/mask/polygon.py +++ b/src/erlab/analysis/mask/polygon.py @@ -160,6 +160,8 @@ def bounded_side_bool( return True case Side.ON_BOUNDARY: return boundary + case _: + return False @numba.njit(nogil=True, cache=True) diff --git a/src/erlab/analysis/utilities.py b/src/erlab/analysis/utilities.py index a70a006e..b3bb5131 100644 --- a/src/erlab/analysis/utilities.py +++ b/src/erlab/analysis/utilities.py @@ -2,6 +2,7 @@ import itertools import warnings +from typing import cast import numpy as np import scipy.ndimage @@ -85,7 +86,7 @@ def shift( f"Dimension {dim} in shift array has different size than input array" ) - domain_indices: list[int] = [darr.get_axis_num(ax) for ax in shift.dims] + domain_indices: tuple[int, ...] = darr.get_axis_num(shift.dims) # `along` must be evenly spaced and monotonic increasing out = darr.sortby(along).copy() @@ -96,7 +97,7 @@ def shift( if shift_coords: # We first apply the integer part of the average shift to the coords - rigid_shift = np.round(shift.values.mean()) + rigid_shift: float = np.round(shift.values.mean()) shift = shift - rigid_shift # Apply coordinate shift @@ -104,7 +105,7 @@ def shift( # The bounds of the remaining shift values are used to pad the data nshift_min, nshift_max = shift.values.min(), shift.values.max() - pads: tuple[int] = min(0, round(nshift_min)), max(0, round(nshift_max)) + pads: tuple[int, int] = min(0, round(nshift_min)), max(0, round(nshift_max)) # Construct new coordinate array new_along = np.linspace( @@ -114,21 +115,24 @@ def shift( ) # Pad the data and assign new coordinates - out = out.pad({along: np.abs(pads)}, mode="constant", constant_values=np.nan) + out = out.pad( + {along: tuple(np.abs(pads))}, mode="constant", constant_values=np.nan + ) out = out.assign_coords({along: new_along}) for idxs in itertools.product(*[range(darr.shape[i]) for i in domain_indices]): # Construct slices for indexing - slices = [slice(None)] * darr.ndim - for domain_index, i in zip(domain_indices, idxs): - slices[domain_index] = i - slices = tuple(slices) + _slices: list[slice | int] = [slice(None)] * darr.ndim + for domain_index, i in zip(domain_indices, idxs, strict=True): + _slices[domain_index] = i + + slices: tuple[slice | int, ...] = tuple(_slices) # Initialize arguments to `scipy.ndimage.shift` input = out[slices] - shifts = [0] * input.ndim - shift_val: float = float(shift.isel(dict(zip(shift.dims, idxs)))) - shifts[input.get_axis_num(along)] = shift_val + shifts: list[float] = [0.0] * input.ndim + shift_val: float = float(shift.isel(dict(zip(shift.dims, idxs, strict=True)))) + shifts[cast(int, input.get_axis_num(along))] = shift_val # Apply shift out[slices] = scipy.ndimage.shift(input.values, shifts, **shift_kwargs) diff --git a/src/erlab/characterization/__init__.py b/src/erlab/characterization/__init__.py index aab49998..cbf7ee98 100644 --- a/src/erlab/characterization/__init__.py +++ b/src/erlab/characterization/__init__.py @@ -1,15 +1,8 @@ -""" -Data import and analysis for characterization experiments. - -.. currentmodule:: erlab.characterization - -Modules -======= - -.. autosummary:: - :toctree: generated - - xrd - resistance - -""" +import warnings +from erlab.io.characterization import xrd, resistance # noqa: F401 + +warnings.warn( + "`erlab.characterization` is deprecated. Use `erlab.io.characterization` instead", + DeprecationWarning, + stacklevel=2, +) diff --git a/src/erlab/interactive/bzplot.py b/src/erlab/interactive/bzplot.py index 6b3829f7..b1fcfabb 100644 --- a/src/erlab/interactive/bzplot.py +++ b/src/erlab/interactive/bzplot.py @@ -49,11 +49,20 @@ def __init__( param_type = "bvec" if param_type == "lattice": + if len(params) != 6: + raise TypeError("Lattice parameters must be a 6-tuple.") + bvec = to_reciprocal(abc2avec(*params)) - elif param_type == "avec": - bvec = to_reciprocal(params) - elif param_type == "bvec": - bvec = params + else: + if not isinstance(params, np.ndarray): + raise TypeError("Lattice vectors must be a numpy array.") + if params.shape != (3, 3): + raise TypeError("Lattice vectors must be a 3 by 3 numpy array.") + + if param_type == "avec": + bvec = to_reciprocal(params) + elif param_type == "bvec": + bvec = params self.controls = None self.plot = BZPlotWidget(bvec) diff --git a/src/erlab/interactive/colors.py b/src/erlab/interactive/colors.py index 4df7987c..a506a3de 100644 --- a/src/erlab/interactive/colors.py +++ b/src/erlab/interactive/colors.py @@ -16,14 +16,17 @@ import weakref from collections.abc import Iterable, Sequence -from typing import Literal +from typing import TYPE_CHECKING, Literal -import matplotlib.colors as mcolors +import matplotlib.colors import numpy as np import numpy.typing as npt import pyqtgraph as pg from qtpy import QtCore, QtGui, QtWidgets +if TYPE_CHECKING: + from matplotlib.typing import ColorType + EXCLUDED_CMAPS: tuple[str, ...] = ( "prism", "tab10", @@ -156,16 +159,16 @@ class ColorMapGammaWidget(QtWidgets.QWidget): def __init__( self, - parent: QtWidgets.QWidget = None, + parent: QtWidgets.QWidget | None = None, value: float = 1.0, slider_cls: type | None = None, spin_cls: type | None = None, ): super().__init__(parent=parent) - self.setLayout(QtWidgets.QHBoxLayout(self)) - self.layout().setContentsMargins(0, 0, 0, 0) - - self.layout().setSpacing(3) + layout = QtWidgets.QHBoxLayout(self) + self.setLayout(layout) + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(3) if slider_cls is None: slider_cls = QtWidgets.QSlider @@ -202,9 +205,9 @@ def __init__( ) self.slider.valueChanged.connect(self.slider_changed) - self.layout().addWidget(self.label) - self.layout().addWidget(self.spin) - self.layout().addWidget(self.slider) + layout.addWidget(self.label) + layout.addWidget(self.spin) + layout.addWidget(self.slider) def value(self) -> float: return self.spin.value() @@ -219,13 +222,13 @@ def spin_changed(self, value: float): self.slider.blockSignals(False) self.valueChanged.emit(value) - def slider_changed(self, value: float): + def slider_changed(self, value: float | int): self.spin.setValue(self.gamma_scale_inv(value)) def gamma_scale(self, y: float) -> int: return round(1e4 * np.log10(y)) - def gamma_scale_inv(self, x: int) -> float: + def gamma_scale_inv(self, x: float | int) -> float: return np.power(10, x * 1e-4) @@ -248,7 +251,7 @@ class BetterImageItem(pg.ImageItem): sigColorChanged = QtCore.Signal() #: :meta private: - def __init__(self, image: npt.NDArray = None, **kwargs): + def __init__(self, image: npt.NDArray | None = None, **kwargs): super().__init__(image, **kwargs) def set_colormap( @@ -279,7 +282,7 @@ class BetterColorBarItem(pg.PlotItem): def __init__( self, parent: QtWidgets.QWidget | None = None, - image: Sequence[BetterImageItem] | BetterImageItem | None = None, + image: Iterable[BetterImageItem] | BetterImageItem | None = None, autoLevels: bool = False, limits: tuple[float, float] | None = None, pen: QtGui.QPen | str = "c", @@ -386,15 +389,14 @@ def setLimits(self, limits: tuple[float, float] | None): if self._primary_image is not None: self.limit_changed() - def addImage(self, image: Sequence[BetterImageItem] | BetterImageItem): - # if isinstance(image, BetterImageItem): - if not np.iterable(image): + def addImage(self, image: Iterable[BetterImageItem] | BetterImageItem): + if not isinstance(image, Iterable): self._images.add(weakref.ref(image)) else: for img in image: self._images.add(weakref.ref(img)) - def removeImage(self, image: Sequence[BetterImageItem] | BetterImageItem): + def removeImage(self, image: Iterable[BetterImageItem] | BetterImageItem): if isinstance(image, Iterable): for img in image: self._images.remove(weakref.ref(img)) @@ -403,7 +405,7 @@ def removeImage(self, image: Sequence[BetterImageItem] | BetterImageItem): def setImageItem( self, - image: Sequence[BetterImageItem] | BetterImageItem, + image: Iterable[BetterImageItem] | BetterImageItem, insert_in: pg.PlotItem | None = None, ): self.addImage(image) @@ -527,9 +529,7 @@ def mouseDragEvent(self, ev): ev.ignore() -def color_to_QColor( - c: str | tuple[float, ...], alpha: float | None = None -) -> QtGui.QColor: +def color_to_QColor(c: ColorType, alpha: float | None = None) -> QtGui.QColor: """Convert a matplotlib color to a :class:`PySide6.QtGui.QColor`. Parameters @@ -546,7 +546,7 @@ def color_to_QColor( PySide6.QtGui.QColor """ - return QtGui.QColor.fromRgbF(*mcolors.to_rgba(c, alpha=alpha)) + return QtGui.QColor.fromRgbF(*matplotlib.colors.to_rgba(c, alpha=alpha)) def pg_colormap_names( @@ -700,5 +700,5 @@ def pg_colormap_to_QPixmap( cmap_arr = cmap.getLookupTable(0, 1, w, alpha=True)[:, None] # print(cmap_arr.shape) - img = QtGui.QImage(cmap_arr, w, 1, QtGui.QImage.Format_RGBA8888) + img = QtGui.QImage(cmap_arr, w, 1, QtGui.QImage.Format.Format_RGBA8888) return QtGui.QPixmap.fromImage(img).scaled(w, h) diff --git a/src/erlab/interactive/curvefittingtool.py b/src/erlab/interactive/curvefittingtool.py index c4d62f9f..6e66d41a 100644 --- a/src/erlab/interactive/curvefittingtool.py +++ b/src/erlab/interactive/curvefittingtool.py @@ -1,5 +1,6 @@ import copy import sys +from typing import cast import lmfit import pyqtgraph as pg @@ -54,7 +55,7 @@ class SinglePeakWidget(ParameterGroup): - VALID_LINESHAPE = ["lorentzian", "gaussian"] + VALID_LINESHAPE: tuple[str, ...] = ("lorentzian", "gaussian") def __init__(self, peak_index): self.peak_index = peak_index @@ -96,7 +97,7 @@ def param_dict(self): @property def peak_shape(self) -> str: - return self.values["Peak Shape"] + return str(self.values["Peak Shape"]) class PlotPeakItem(pg.PlotCurveItem): @@ -200,7 +201,7 @@ def __init__(self, data, n_bands: int = 1, parameters=None, *args, **kwargs): self.qapp = QtCore.QCoreApplication.instance() if not self.qapp: self.qapp = QtWidgets.QApplication(sys.argv) - self.qapp.setStyle("Fusion") + cast(QtWidgets.QApplication, self.qapp).setStyle("Fusion") super().__init__() self.resize(720, 360) @@ -299,8 +300,8 @@ def __init__(self, data, n_bands: int = 1, parameters=None, *args, **kwargs): self.fitplot = self.plotwidget.plot() self.fitplot.setPen(pg.mkPen("c")) - self.peakcurves = [] - self.peaklines = [] + self.peakcurves: list[PlotPeakItem] = [] + self.peaklines: list[PlotPeakPosition] = [] self.refresh_n_peaks() @@ -427,7 +428,7 @@ def set_params(self, params: dict): } ) for i in range(self.n_bands): - self._params_peak.widget(i).set_values( + self._params_peak.widget(i).set_values( # type: ignore[union-attr] **{k[3:]: v for k, v in params.items() if k.startswith(f"p{i}")} ) @@ -455,7 +456,7 @@ def __init__(self, data, n_bands: int = 1, parameters=None, *args, **kwargs): self.qapp = QtCore.QCoreApplication.instance() if not self.qapp: self.qapp = QtWidgets.QApplication(sys.argv) - self.qapp.setStyle("Fusion") + cast(QtWidgets.QApplication, self.qapp).setStyle("Fusion") super().__init__() self.resize(720, 360) @@ -534,8 +535,8 @@ def __init__(self, data, n_bands: int = 1, parameters=None, *args, **kwargs): self.fitplot = self.plotwidget.plot() self.fitplot.setPen(pg.mkPen("c")) - self.peakcurves = [] - self.peaklines = [] + self.peakcurves: list[PlotPeakItem] = [] + self.peaklines: list[PlotPeakPosition] = [] self.refresh_n_peaks() @@ -660,7 +661,7 @@ def set_params(self, params: dict): } ) for i in range(self.n_bands): - self._params_peak.widget(i).set_values( + self._params_peak.widget(i).set_values( # type: ignore[union-attr] **{k[3:]: v for k, v in params.items() if k.startswith(f"p{i}")} ) diff --git a/src/erlab/interactive/derivative.py b/src/erlab/interactive/derivative.py index 471b270f..89e619f8 100644 --- a/src/erlab/interactive/derivative.py +++ b/src/erlab/interactive/derivative.py @@ -5,6 +5,7 @@ import functools import os import sys +from typing import TYPE_CHECKING, cast import numpy as np import pyqtgraph as pg @@ -26,14 +27,17 @@ xImageItem, ) +if TYPE_CHECKING: + from collections.abc import Hashable + class DerivativeTool( - *uic.loadUiType(os.path.join(os.path.dirname(__file__), "dtool.ui")) + *uic.loadUiType(os.path.join(os.path.dirname(__file__), "dtool.ui")) # type: ignore[misc] ): def __init__(self, data: xr.DataArray, *, data_name: str | None = None): if data_name is None: try: - data_name = varname.argname("data", func=self.__init__, vars_only=False) + data_name = varname.argname("data", func=self.__init__, vars_only=False) # type: ignore[misc] except varname.VarnameRetrievingError: data_name = "data" @@ -50,8 +54,8 @@ def __init__(self, data: xr.DataArray, *, data_name: str | None = None): self.data: xr.DataArray = parse_data(data) self._result: xr.DataArray = self.data.copy() - self.xdim: str = self.data.dims[1] - self.ydim: str = self.data.dims[0] + self.xdim: Hashable = self.data.dims[1] + self.ydim: Hashable = self.data.dims[0] self.xinc: float = abs(float(self.data[self.xdim][1] - self.data[self.xdim][0])) self.yinc: float = abs(float(self.data[self.ydim][1] - self.data[self.ydim][0])) @@ -138,11 +142,11 @@ def processed_data(self) -> xr.DataArray: if self.interp_group.isChecked(): out = self.data.interp( { - self.xdim: np.linspace( - *self.data[self.xdim][[0, -1]], self.nx_spin.value() + self.xdim: np.linspace( # type: ignore[call-overload] + *self.data[self.xdim].values[[0, -1]], self.nx_spin.value() ), - self.ydim: np.linspace( - *self.data[self.ydim][[0, -1]], self.ny_spin.value() + self.ydim: np.linspace( # type: ignore[call-overload] + *self.data[self.ydim].values[[0, -1]], self.ny_spin.value() ), } ) @@ -221,7 +225,9 @@ def copy_code(self): arg_dict = { dim: f"|np.linspace(*{data_name}['{dim}'][[0, -1]], {n})|" for dim, n in zip( - [self.xdim, self.ydim], [self.nx_spin.value(), self.ny_spin.value()] + [self.xdim, self.ydim], + [self.nx_spin.value(), self.ny_spin.value()], + strict=True, ) } lines.append( @@ -240,6 +246,7 @@ def copy_code(self): np.round(s.value(), s.decimals()) for s in (self.sx_spin, self.sy_spin) ], + strict=True, ) ) } @@ -310,7 +317,7 @@ def dtool(data, data_name: str | None = None, *, execute: bool | None = None): if not qapp: qapp = QtWidgets.QApplication(sys.argv) - qapp.setStyle("Fusion") + cast(QtWidgets.QApplication, qapp).setStyle("Fusion") win = DerivativeTool(data, data_name=data_name) win.show() diff --git a/src/erlab/interactive/fermiedge.py b/src/erlab/interactive/fermiedge.py index e18d571c..ca195580 100644 --- a/src/erlab/interactive/fermiedge.py +++ b/src/erlab/interactive/fermiedge.py @@ -18,6 +18,7 @@ ParameterGroup, ROIControls, gen_function_code, + xImageItem, ) from erlab.parallel import joblib_progress_qt @@ -156,6 +157,9 @@ def __init__( self._argnames["data_corr"] = "data_corr" self.data_corr = data_corr + self.hists: pg.HistogramLUTItem + self.axes: list[pg.PlotItem] + self.images: list[xImageItem] self.axes[1].setVisible(False) self.hists[1].setVisible(False) @@ -169,7 +173,7 @@ def __init__( self.params_roi = ROIControls(self.add_roi(0)) self.params_edge = ParameterGroup( - **{ + { "T (K)": {"qwtype": "dblspin", "value": temp, "range": (0.0, 400.0)}, "Fix T": {"qwtype": "chkbox", "checked": True}, "Bin x": {"qwtype": "spin", "value": 1, "minimum": 1}, @@ -195,7 +199,7 @@ def __init__( self.params_edge.widgets["Fast"].stateChanged.connect(self._toggle_fast) self.params_poly = ParameterGroup( - **{ + { "Degree": {"qwtype": "spin", "value": 4, "range": (1, 20)}, "Method": {"qwtype": "combobox", "items": LMFIT_METHODS}, "Scale cov": {"qwtype": "chkbox", "checked": True}, @@ -220,7 +224,7 @@ def __init__( ) self.params_spl = ParameterGroup( - **{ + { "Auto": {"qwtype": "chkbox", "checked": True}, "lambda": { "qwtype": "dblspin", @@ -299,18 +303,18 @@ def __init__( self.axes[0].disableAutoRange() # Setup time calculation - self.start_time: float | None = None - self.step_times: list[float] = [] + self.start_time: float + self.step_times: list[float] # Setup progress bar - self.progress = QtWidgets.QProgressDialog( + self.progress: QtWidgets.QProgressDialog = QtWidgets.QProgressDialog( labelText="Fitting...", minimum=0, parent=self, minimumDuration=0, windowModality=QtCore.Qt.WindowModal, ) - self.pbar = QtWidgets.QProgressBar() + self.pbar: QtWidgets.QProgressBar = QtWidgets.QProgressBar() self.progress.setBar(self.pbar) self.progress.setFixedSize(self.progress.size()) self.progress.setCancelButtonText("Abort!") @@ -360,7 +364,7 @@ def iterated(self, n: int): @QtCore.Slot() def perform_edge_fit(self): self.start_time = time.perf_counter() - self.step_times: list[float] = [0.0] + self.step_times = [0.0] self.progress.setVisible(True) self.params_roi.draw_button.setChecked(False) diff --git a/src/erlab/interactive/imagetool/__init__.py b/src/erlab/interactive/imagetool/__init__.py index 2b40fdcb..cd24f379 100644 --- a/src/erlab/interactive/imagetool/__init__.py +++ b/src/erlab/interactive/imagetool/__init__.py @@ -21,8 +21,10 @@ import gc import sys -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any, cast +import numpy as np +import numpy.typing as npt import xarray as xr from qtpy import QtCore, QtWidgets @@ -38,23 +40,16 @@ if TYPE_CHECKING: from collections.abc import Callable, Sequence - import numpy as np - import numpy.typing as npt - from erlab.interactive.imagetool.slicer import ArraySlicer def itool( - data: ( - Sequence[xr.DataArray | npt.ArrayLike[np.floating]] - | xr.DataArray - | npt.ArrayLike[np.floating] - ), + data: Sequence[xr.DataArray | npt.NDArray] | xr.DataArray | npt.NDArray, link: bool = False, link_colors: bool = True, execute: bool | None = None, **kwargs, -): +) -> ImageTool | list[ImageTool] | None: """Create and display an ImageTool window. Parameters @@ -78,7 +73,7 @@ def itool( Returns ------- - ImageTool or tuple of ImageTool + ImageTool or list of ImageTool The created ImageTool window(s). Notes @@ -93,29 +88,32 @@ def itool( >>> itool(data_list, link=True) """ - qapp: QtWidgets.QApplication = QtWidgets.QApplication.instance() + qapp = QtWidgets.QApplication.instance() if not qapp: qapp = QtWidgets.QApplication(sys.argv) - qapp.setStyle("Fusion") - - if isinstance(data, list | tuple): - win = () - for d in data: - win += (ImageTool(d, **kwargs),) - for w in win: - w.show() - win[-1].activateWindow() - win[-1].raise_() - - if link: - linker = SlicerLinkProxy( # noqa: F841 - *[w.slicer_area for w in win], link_colors=link_colors - ) - else: - win = ImageTool(data, **kwargs) - win.show() - win.raise_() - win.activateWindow() + + if isinstance(qapp, QtWidgets.QApplication): + qapp.setStyle("Fusion") + + if isinstance(data, np.ndarray | xr.DataArray): + data = cast(list[npt.NDArray | xr.DataArray], [data]) + + itool_list = [ImageTool(d, **kwargs) for d in data] + + for w in itool_list: + w.show() + + if len(itool_list) == 0: + raise ValueError("No data provided") + + itool_list[-1].activateWindow() + itool_list[-1].raise_() + + if link: + linker = SlicerLinkProxy( # noqa: F841 + *[w.slicer_area for w in itool_list], link_colors=link_colors + ) + if execute is None: execute = True try: @@ -127,13 +125,14 @@ def itool( start_event_loop_qt4(qapp) except NameError: pass + if execute: qapp.exec() - del win + del itool_list gc.collect() return None - return win + return itool_list class BaseImageTool(QtWidgets.QMainWindow): @@ -231,7 +230,7 @@ def colorAct(self): ) def _generate_menu_kwargs(self) -> dict: - menu_kwargs = { + menu_kwargs: dict[str, Any] = { "fileMenu": { "title": "&File", "actions": { @@ -340,6 +339,7 @@ def _generate_menu_kwargs(self) -> dict: ), (1, 1, 0, 0) * 2, (1, -1, 1, -1, 10, -10, 10, -10), + strict=True, ) ): menu_kwargs["viewMenu"]["actions"]["cursorMoveMenu"]["actions"][ @@ -373,6 +373,7 @@ def _generate_menu_kwargs(self) -> dict: ), (1, 1, 0, 0) * 2, (1, -1, 1, -1, 10, -10, 10, -10), + strict=True, ) ): menu_kwargs["viewMenu"]["actions"]["cursorMoveMenu"]["actions"][ @@ -402,7 +403,9 @@ def refreshMenus(self): self.action_dict["snapCursorAct"].blockSignals(False) cmap_props = self.slicer_area.colormap_properties - for ca, k in zip(self.colorAct, ["reversed", "highContrast", "zeroCentered"]): + for ca, k in zip( + self.colorAct, ["reversed", "highContrast", "zeroCentered"], strict=True + ): ca.blockSignals(True) ca.setChecked(cmap_props[k]) ca.blockSignals(False) diff --git a/src/erlab/interactive/imagetool/_deprecated/imagetool_mpl.py b/src/erlab/interactive/imagetool/_deprecated/imagetool_mpl.py index c8b3af92..6c1fee6d 100644 --- a/src/erlab/interactive/imagetool/_deprecated/imagetool_mpl.py +++ b/src/erlab/interactive/imagetool/_deprecated/imagetool_mpl.py @@ -839,7 +839,9 @@ def update_spans(self): span.set_xy(get_xy_y(*domain)) span.set_visible(self.visible) if self.useblit: - for i, span in list(zip(self.span_ax_index[axis], self.spans[axis])): + for i, span in list( + zip(self.span_ax_index[axis], self.spans[axis], strict=True) + ): self.axes[i].draw_artist(span) def get_index_of_value(self, axis, val): @@ -968,7 +970,9 @@ def _update(self): # self.pool(delayed(self.axes[i].draw_artist)(art) for i, art in list(zip( # (0, 1, 4, 0, 2, 5, 3, 5, 4), self.cursors))) else: - for i, art in list(zip(self.ax_index, self.all + self.scaling_axes)): + for i, art in list( + zip(self.ax_index, self.all + self.scaling_axes, strict=True) + ): self.axes[i].draw_artist(art) if any(self.averaged): self.update_spans() diff --git a/src/erlab/interactive/imagetool/_deprecated/imagetool_old.py b/src/erlab/interactive/imagetool/_deprecated/imagetool_old.py index 5189482c..17c805e0 100644 --- a/src/erlab/interactive/imagetool/_deprecated/imagetool_old.py +++ b/src/erlab/interactive/imagetool/_deprecated/imagetool_old.py @@ -1154,7 +1154,7 @@ def _initialize_layout( ) else: raise NotImplementedError("Only supports 2D, 3D, and 4D arrays.") - for i, (p, sel) in enumerate(zip(self.axes, valid_selection)): + for i, (p, sel) in enumerate(zip(self.axes, valid_selection, strict=True)): p.setDefaultPadding(0) for axis in ["left", "bottom", "right", "top"]: p.getAxis(axis).setTickFont(font) diff --git a/src/erlab/interactive/imagetool/controls.py b/src/erlab/interactive/imagetool/controls.py index 12b4515a..e069c93a 100644 --- a/src/erlab/interactive/imagetool/controls.py +++ b/src/erlab/interactive/imagetool/controls.py @@ -13,44 +13,47 @@ import pyqtgraph as pg import qtawesome as qta from qtpy import QtCore, QtGui, QtWidgets - +import types from erlab.interactive.colors import ColorMapComboBox, ColorMapGammaWidget from erlab.interactive.utilities import BetterSpinBox if TYPE_CHECKING: import xarray as xr + from collections.abc import Mapping from erlab.interactive.imagetool.core import ImageSlicerArea from erlab.interactive.imagetool.slicer import ArraySlicer class IconButton(QtWidgets.QPushButton): - ICON_ALIASES = { - "invert": "mdi6.invert-colors", - "invert_off": "mdi6.invert-colors-off", - "contrast": "mdi6.contrast-box", - "lock": "mdi6.lock", - "unlock": "mdi6.lock-open-variant", - "bright_auto": "mdi6.brightness-auto", - "bright_percent": "mdi6.brightness-percent", - "colorbar": "mdi6.gradient-vertical", - "transpose_0": "mdi6.arrow-top-left-bottom-right", - "transpose_1": "mdi6.arrow-up-down", - "transpose_2": "mdi6.arrow-left-right", - "transpose_3": "mdi6.axis-z-arrow", - "snap": "mdi6.grid", - "snap_off": "mdi6.grid-off", - "palette": "mdi6.palette-advanced", - "styles": "mdi6.palette-swatch", - "layout": "mdi6.page-layout-body", - "zero_center": "mdi6.format-vertical-align-center", - "table_eye": "mdi6.table-eye", - "plus": "mdi6.plus", - "minus": "mdi6.minus", - "reset": "mdi6.backup-restore", - # all_cursors="mdi6.checkbox-multiple-outline", - "all_cursors": "mdi6.select-multiple", - } + ICON_ALIASES: Mapping[str, str] = types.MappingProxyType( + { + "invert": "mdi6.invert-colors", + "invert_off": "mdi6.invert-colors-off", + "contrast": "mdi6.contrast-box", + "lock": "mdi6.lock", + "unlock": "mdi6.lock-open-variant", + "bright_auto": "mdi6.brightness-auto", + "bright_percent": "mdi6.brightness-percent", + "colorbar": "mdi6.gradient-vertical", + "transpose_0": "mdi6.arrow-top-left-bottom-right", + "transpose_1": "mdi6.arrow-up-down", + "transpose_2": "mdi6.arrow-left-right", + "transpose_3": "mdi6.axis-z-arrow", + "snap": "mdi6.grid", + "snap_off": "mdi6.grid-off", + "palette": "mdi6.palette-advanced", + "styles": "mdi6.palette-swatch", + "layout": "mdi6.page-layout-body", + "zero_center": "mdi6.format-vertical-align-center", + "table_eye": "mdi6.table-eye", + "plus": "mdi6.plus", + "minus": "mdi6.minus", + "reset": "mdi6.backup-restore", + # all_cursors="mdi6.checkbox-multiple-outline", + "all_cursors": "mdi6.select-multiple", + } + ) def __init__(self, on: str | None = None, off: str | None = None, **kwargs): self.icon_key_on = None @@ -88,18 +91,22 @@ def refresh_icons(self): if self.icon_key_on is not None: self.setIcon(self.get_icon(self.icon_key_on)) - def changeEvent(self, evt: QtCore.QEvent): # handles dark mode - if evt.type() == QtCore.QEvent.Type.PaletteChange: + def changeEvent(self, evt: QtCore.QEvent | None): # handles dark mode + if evt is not None and evt.type() == QtCore.QEvent.Type.PaletteChange: qta.reset_cache() self.refresh_icons() super().changeEvent(evt) -def clear_layout(layout: QtWidgets.QLayout): +def clear_layout(layout: QtWidgets.QLayout | None) -> None: + if layout is None: + return while layout.count(): child = layout.takeAt(0) - if child.widget(): - child.widget().deleteLater() + if child is not None: + w = child.widget() + if w is not None: + w.deleteLater() class ItoolControlsBase(QtWidgets.QWidget): @@ -108,7 +115,7 @@ def __init__( ): super().__init__(*args, **kwargs) self._slicer_area = slicer_area - self.sub_controls = [] + self.sub_controls: list[QtWidgets.QWidget] = [] self.initialize_layout() self.initialize_widgets() self.connect_signals() diff --git a/src/erlab/interactive/imagetool/core.py b/src/erlab/interactive/imagetool/core.py index 832ee263..56998947 100644 --- a/src/erlab/interactive/imagetool/core.py +++ b/src/erlab/interactive/imagetool/core.py @@ -10,7 +10,7 @@ import os import time import weakref -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Literal, TypedDict, cast import numpy as np import numpy.typing as npt @@ -32,6 +32,14 @@ from pyqtgraph.graphicsItems.ViewBox import ViewBoxMenu from pyqtgraph.GraphicsScene import mouseEvents + class ColorMapProperties(TypedDict): + cmap: str | pg.ColorMap + gamma: float + reversed: bool + highContrast: bool + zeroCentered: bool + + suppressnanwarning = np.testing.suppress_warnings() suppressnanwarning.filter(RuntimeWarning, r"All-NaN (slice|axis) encountered") @@ -118,11 +126,13 @@ def link_slicer( indices If `True`, the input argument named `value` given to `func` are interpreted as indices, and will be converted to appropriate values for other instances of - `ImageSlicerArea`. The behavior of this conversion is determined by `steps`. + `ImageSlicerArea`. The behavior of this conversion is determined by `steps`. If + `True`, An input argument named `axis` of type integer must be present in the + decorated method to determine the axis along which the index is to be changed. steps - If `False`, considers `value` as an absolute index. If `True`, considers - `value` as a relative value such as the number of steps or bins. See the - implementation of `SlicerLinkProxy` for more information. + If `False`, considers `value` as an absolute index. If `True`, considers `value` + as a relative value such as the number of steps or bins. See the implementation + of `SlicerLinkProxy` for more information. color Boolean whether the decorated method is related to visualization, such as colormap control. @@ -167,7 +177,7 @@ class SlicerLinkProxy: """ - def __init__(self, *slicers: list[ImageSlicerArea], link_colors: bool = True): + def __init__(self, *slicers: ImageSlicerArea, link_colors: bool = True): self.link_colors = link_colors self._slicers: set[ImageSlicerArea] = set() for s in slicers: @@ -228,20 +238,18 @@ def convert_args( steps: bool, ): if indices: - axis: int | None = args.get("axis") - index: int | None = args.get("value") + index: int | None = args.get("value", None) if index is not None: + axis: int | None = args.get("axis") + if axis is None: - args["value"] = [ - self.convert_index(source, target, a, i, steps) - for (a, i) in zip(axis, index) - ] - else: - args["value"] = self.convert_index( - source, target, axis, index, steps + raise ValueError( + "Axis argument not found in decorated method with `indices=True`" ) + args["value"] = self.convert_index(source, target, axis, index, steps) + args["__slicer_skip_sync"] = True # passed onto the decorator return args @@ -309,7 +317,7 @@ class ImageSlicerArea(QtWidgets.QWidget): """ - COLORS: list[QtGui.QColor] = [ + COLORS: tuple[QtGui.QColor, ...] = ( pg.mkColor(0.8), pg.mkColor("y"), pg.mkColor("m"), @@ -317,7 +325,7 @@ class ImageSlicerArea(QtWidgets.QWidget): pg.mkColor("g"), pg.mkColor("r"), pg.mkColor("b"), - ] #: List of :class:`PySide6.QtGui.QColor` containing colors for multiple cursors. + ) #: :class:`PySide6.QtGui.QColor`\ s for multiple cursors. sigDataChanged = QtCore.Signal() #: :meta private: sigCurrentCursorChanged = QtCore.Signal(int) #: :meta private: @@ -362,9 +370,11 @@ def __init__( self.bench = bench - self.setLayout(QtWidgets.QHBoxLayout()) - self.layout().setContentsMargins(0, 0, 0, 0) - self.layout().setSpacing(0) + layout = QtWidgets.QHBoxLayout() + self.setLayout(layout) + + layout.setContentsMargins(0, 0, 0, 0) + layout.setSpacing(0) self._splitters = ( QtWidgets.QSplitter(QtCore.Qt.Orientation.Vertical), @@ -383,7 +393,7 @@ def __init__( # s.setPalette(palette) # print(s.handleWidth()) # pass - self.layout().addWidget(self._splitters[0]) + layout.addWidget(self._splitters[0]) for i, j in ((0, 1), (1, 2), (1, 3), (0, 4), (4, 5), (4, 6)): self._splitters[i].addWidget(self._splitters[j]) _sync_splitters(self._splitters[1], self._splitters[4]) @@ -391,11 +401,11 @@ def __init__( self.cursor_colors: list[QtGui.QColor] = [self.COLORS[0]] self._colorbar = ItoolColorBar(self) - self.layout().addWidget(self._colorbar) + layout.addWidget(self._colorbar) self._colorbar.setVisible(False) pkw = {"image_cls": image_cls, "plotdata_cls": plotdata_cls} - self.manual_limits: dict[str | list[float]] = {} + self.manual_limits: dict[str, list[list[float]]] = {} self._plots: tuple[ItoolGraphicsLayoutWidget, ...] = ( ItoolGraphicsLayoutWidget(self, image=True, display_axis=(0, 1), **pkw), ItoolGraphicsLayoutWidget(self, display_axis=(0,), **pkw), @@ -414,7 +424,7 @@ def __init__( for i in (5, 2): self._splitters[6].addWidget(self._plots[i]) - self.qapp: QtWidgets.QApplication = QtWidgets.QApplication.instance() + self.qapp = cast(QtWidgets.QApplication, QtWidgets.QApplication.instance()) self.qapp.aboutToQuit.connect(self.on_close) cmap_reversed = False @@ -426,7 +436,7 @@ def __init__( if cmap.startswith("cet_CET"): cmap = cmap[4:] - self.colormap_properties: dict[str, str | pg.ColorMap | float | bool] = { + self.colormap_properties: ColorMapProperties = { "cmap": cmap, "gamma": gamma, "reversed": cmap_reversed, @@ -488,15 +498,18 @@ def slices(self) -> tuple[ItoolPlotItem, ...]: return tuple(self.get_axes(ax) for ax in (4, 5)) elif self.data.ndim == 4: return tuple(self.get_axes(ax) for ax in (4, 5, 7)) + else: + raise ValueError("Data must have 2 to 4 dimensions") @property def profiles(self) -> tuple[ItoolPlotItem, ...]: if self.data.ndim == 2: - profile_axes = (1, 2) + profile_axes = [1, 2] elif self.data.ndim == 3: - profile_axes = (1, 2, 3) + profile_axes = [1, 2, 3] else: - profile_axes = (1, 2, 3, 6) + profile_axes = [1, 2, 3, 6] + return tuple(self.get_axes(ax) for ax in profile_axes) @property @@ -639,7 +652,7 @@ def set_data( if hasattr(self, "_array_slicer"): self._array_slicer.set_array(self._data, reset=True) else: - self._array_slicer = ArraySlicer(self._data) + self._array_slicer: ArraySlicer = ArraySlicer(self._data) while self.n_cursors != n_cursors_old: self.array_slicer.add_cursor(update=False) @@ -813,7 +826,7 @@ def set_colormap( @QtCore.Slot(bool) def lock_levels(self, lock: bool): - self.levels_locked: bool = lock + self.levels_locked = lock if self.levels_locked: levels = self.array_slicer.limits @@ -870,7 +883,7 @@ def adjust_layout( font = QtGui.QFont() font.setPointSizeF(float(font_size)) - valid_axis: tuple[tuple[bool, bool, bool, bool]] = ( + valid_axis: tuple[tuple[Literal[0, 1], ...], ...] = ( (1, 0, 0, 1), (1, 1, 0, 0), (0, 0, 1, 1), @@ -905,7 +918,7 @@ def adjust_layout( ] if self.data.ndim == 4: sizes[3] = (0, 0, (r0 + r1 - d)) - for split, sz in zip(self._splitters, sizes): + for split, sz in zip(self._splitters, sizes, strict=True): split.setSizes(tuple(round(s * scale) for s in sz)) for i, sel in enumerate(valid_axis): @@ -940,22 +953,23 @@ def toggle_snap(self, value: bool | None = None): self.array_slicer.snap_to_data = value self.sigViewOptionChanged.emit() - def changeEvent(self, evt: QtCore.QEvent): - if evt.type() == QtCore.QEvent.Type.PaletteChange: - self.qapp.setStyle(self.qapp.style().name()) + def changeEvent(self, evt: QtCore.QEvent | None): + if evt is not None and evt.type() == QtCore.QEvent.Type.PaletteChange: + style = self.qapp.style() + if style is not None: + self.qapp.setStyle(style.name()) super().changeEvent(evt) class ItoolCursorLine(pg.InfiniteLine): def __init__(self, *args, **kargs): super().__init__(*args, **kargs) - self.qapp: QtWidgets.QApplication = QtWidgets.QApplication.instance() @property def plotItem(self) -> ItoolPlotItem: return self.parentItem().parentItem().parentItem() - def setBounds(self, bounds: Sequence[float], value: float | None = None): + def setBounds(self, bounds: Sequence[np.floating], value: float | None = None): if bounds[0] > bounds[1]: bounds = list(bounds) bounds.reverse() @@ -970,7 +984,7 @@ def value(self) -> float: def mouseDragEvent(self, ev: mouseEvents.MouseDragEvent): if ( QtCore.Qt.KeyboardModifier.ControlModifier - not in self.qapp.keyboardModifiers() + not in QtWidgets.QApplication.keyboardModifiers() ): if self.movable and ev.button() == QtCore.Qt.MouseButton.LeftButton: if ev.isStart(): @@ -1001,7 +1015,7 @@ def mouseDragEvent(self, ev: mouseEvents.MouseDragEvent): def mouseClickEvent(self, ev: mouseEvents.MouseClickEvent): if ( QtCore.Qt.KeyboardModifier.ControlModifier - not in self.qapp.keyboardModifiers() + not in QtWidgets.QApplication.keyboardModifiers() ): super().mouseClickEvent(ev) else: @@ -1011,7 +1025,7 @@ def mouseClickEvent(self, ev: mouseEvents.MouseClickEvent): def hoverEvent(self, ev): if ( QtCore.Qt.KeyboardModifier.ControlModifier - not in self.qapp.keyboardModifiers() + not in QtWidgets.QApplication.keyboardModifiers() ): super().hoverEvent(ev) else: @@ -1038,7 +1052,7 @@ def __init__(self, axes, cursor: int | None = None): if cursor is None: cursor = 0 self._cursor_index = int(cursor) - self.qapp: QtGui.QGuiApplication = QtGui.QGuiApplication.instance() + self.qapp = QtGui.QGuiApplication.instance() @property def display_axis(self): @@ -1117,13 +1131,19 @@ def refresh_data(self): ) def mouseDragEvent(self, ev: mouseEvents.MouseDragEvent): - if QtCore.Qt.KeyboardModifier.ControlModifier in self.qapp.keyboardModifiers(): + if ( + QtCore.Qt.KeyboardModifier.ControlModifier + in QtWidgets.QApplication.keyboardModifiers() + ): ev.ignore() else: super().mouseDragEvent(ev) def mouseClickEvent(self, ev: mouseEvents.MouseClickEvent): - if QtCore.Qt.KeyboardModifier.ControlModifier in self.qapp.keyboardModifiers(): + if ( + QtCore.Qt.KeyboardModifier.ControlModifier + in QtWidgets.QApplication.keyboardModifiers() + ): ev.ignore() else: super().mouseClickEvent(ev) @@ -1181,14 +1201,16 @@ def __init__( slot=self.process_drag, ) if self.slicer_area.bench: - self._time_start = None - self._time_end = None - self._single_queue = collections.deque([0], maxlen=9) - self._next_queue = collections.deque([0], maxlen=9) + self._time_start: float | None = None + self._time_end: float | None = None + self._single_queue = collections.deque([0.0], maxlen=9) + self._next_queue = collections.deque([0.0], maxlen=9) @property - def axis_dims(self) -> list[str]: - dim_list = [self.slicer_area.data.dims[ax] for ax in self.display_axis] + def axis_dims(self) -> list[str | None]: + dim_list: list[str | None] = [ + str(self.slicer_area.data.dims[ax]) for ax in self.display_axis + ] if not self.is_image: if self.slicer_data_items[-1].is_vertical: dim_list = [None, *dim_list] @@ -1204,7 +1226,10 @@ def refresh_manual_range(self): if self.is_independent: return for dim, auto, rng in zip( - self.axis_dims, self.vb.state["autoRange"], self.vb.state["viewRange"] + self.axis_dims, + self.vb.state["autoRange"], + self.vb.state["viewRange"], + strict=True, ): if dim is not None: if auto: @@ -1218,7 +1243,7 @@ def update_manual_range(self): self.set_range_from(self.slicer_area.manual_limits) def set_range_from(self, limits: dict[str, list[float]], **kwargs): - for dim, key in zip(self.axis_dims, ("xRange", "yRange")): + for dim, key in zip(self.axis_dims, ("xRange", "yRange"), strict=True): if dim is not None: try: kwargs[key] = limits[dim] @@ -1252,7 +1277,7 @@ def process_drag( self, sig: tuple[mouseEvents.MouseDragEvent, QtCore.Qt.KeyboardModifier] ): if self.slicer_area.bench: - if self._time_end is not None: + if self._time_end is not None and self._time_start is not None: self._single_queue.append(1 / (self._time_end - self._time_start)) self._time_end = self._time_start self._time_start = time.perf_counter() @@ -1352,7 +1377,7 @@ def add_cursor(self, update=True): self.cursor_lines.append({}) self.cursor_spans.append({}) - for c, s, ax in zip(cursors, spans, self.display_axis): + for c, s, ax in zip(cursors, spans, self.display_axis, strict=False): self.cursor_lines[-1][ax] = c self.cursor_spans[-1][ax] = s self.addItem(c) @@ -1400,7 +1425,9 @@ def remove_cursor(self, index: int): item = self.slicer_data_items.pop(index) self.removeItem(item) for line, span in zip( - self.cursor_lines.pop(index).values(), self.cursor_spans.pop(index).values() + self.cursor_lines.pop(index).values(), + self.cursor_spans.pop(index).values(), + strict=True, ): self.removeItem(line) self.removeItem(span) @@ -1449,7 +1476,9 @@ def refresh_labels(self): if self.is_image: label_kw = { a: self._get_label_unit(i) - for a, i in zip(("top", "bottom", "left", "right"), (0, 0, 1, 1)) + for a, i in zip( + ("top", "bottom", "left", "right"), (0, 0, 1, 1), strict=True + ) if self.getAxis(a).isVisible() } else: @@ -1540,7 +1569,7 @@ def array_slicer(self) -> ArraySlicer: class ItoolColorBarItem(BetterColorBarItem): - def __init__(self, slicer_area: ImageSlicerArea | None = None, **kwargs): + def __init__(self, slicer_area: ImageSlicerArea, **kwargs): self._slicer_area = slicer_area kwargs.setdefault( "axisItems", @@ -1572,14 +1601,14 @@ def setImageItem(self, *args, **kwargs): class ItoolColorBar(pg.PlotWidget): - def __init__(self, slicer_area: ImageSlicerArea | None = None, **cbar_kw): + def __init__(self, slicer_area: ImageSlicerArea, **cbar_kw): super().__init__( parent=slicer_area, plotItem=ItoolColorBarItem(slicer_area, **cbar_kw) ) self.scene().sigMouseClicked.connect(self.mouseDragEvent) @property - def cb(self) -> BetterColorBarItem: + def cb(self) -> ItoolColorBarItem: return self.plotItem def set_dimensions( diff --git a/src/erlab/interactive/imagetool/fastbinning.py b/src/erlab/interactive/imagetool/fastbinning.py index 6fadf7ad..15990525 100644 --- a/src/erlab/interactive/imagetool/fastbinning.py +++ b/src/erlab/interactive/imagetool/fastbinning.py @@ -6,7 +6,7 @@ __all__ = ["fast_nanmean"] -from collections.abc import Iterable +from collections.abc import Collection import numba import numba.core.registry @@ -315,8 +315,8 @@ def _nanmean_4_123(a: npt.NDArray[np.float32 | np.float64]) -> npt.NDArray[np.fl def fast_nanmean( - a: npt.NDArray[np.float32 | np.float64], axis: int | Iterable[int] | None = None -) -> npt.NDArray[np.float32 | np.float64] | float: + a: npt.NDArray[np.float32 | np.float64], axis: int | Collection[int] | None = None +) -> npt.NDArray[np.float32 | np.float64] | np.float64: """A fast, parallelized arithmetic mean for floating point arrays that ignores NaNs. Parameters @@ -345,8 +345,8 @@ def fast_nanmean( if a.ndim == 1 or axis is None: return _nanmean_all(a) elif a.ndim > 4: - return np.ascontiguousarray(numbagg.nanmean(a, axis)) - if hasattr(axis, "__iter__"): + return np.ascontiguousarray(numbagg.nanmean(a, axis)) # type: ignore[arg-type] + if isinstance(axis, Collection): if len(axis) == a.ndim: return _nanmean_all(a) axis = frozenset(x % a.ndim for x in axis) @@ -356,8 +356,8 @@ def fast_nanmean( def _fast_nanmean_skipcheck( - a: npt.NDArray[np.float32 | np.float64], axis: int | Iterable[int] -) -> npt.NDArray[np.float32 | np.float64] | float: + a: npt.NDArray[np.float32 | np.float64], axis: int | Collection[int] +) -> npt.NDArray[np.float32 | np.float64] | np.float64: """A version of `fast_nanmean` with near-zero overhead. Meant for internal use. Strict assumptions on the input parameters allow skipping some checks. @@ -377,18 +377,8 @@ def _fast_nanmean_skipcheck( The calculated mean. The output array is always C-contiguous. """ - if hasattr(axis, "__iter__"): + if isinstance(axis, Collection): if len(axis) == a.ndim: return _nanmean_all(a) axis = frozenset(axis) return nanmean_funcs[a.ndim][axis](a).astype(a.dtype) - - -if __name__ == "__main__": - for nd, funcs in nanmean_funcs.items(): - x = np.random.RandomState(42).randn(*((30,) * nd)) - for axis, func in funcs.items(): - if isinstance(axis, frozenset): - axis = tuple(axis) - if not np.allclose(np.nanmean(x, axis), fast_nanmean(x, axis)): - print(func) diff --git a/src/erlab/interactive/imagetool/slicer.py b/src/erlab/interactive/imagetool/slicer.py index 0e1a3b04..633d81db 100644 --- a/src/erlab/interactive/imagetool/slicer.py +++ b/src/erlab/interactive/imagetool/slicer.py @@ -15,7 +15,7 @@ from erlab.interactive.imagetool.fastbinning import _fast_nanmean_skipcheck if TYPE_CHECKING: - from collections.abc import Sequence + from collections.abc import Sequence, Hashable import xarray as xr @@ -54,8 +54,8 @@ def _array_rect( y = lims[j][0] - incs[j] w = lims[i][-1] - x h = lims[j][-1] - y - x += 0.5 * incs[i] - y += 0.5 * incs[j] + x += np.float32(0.5 * incs[i]) + y += np.float32(0.5 * incs[j]) return x, y, w, h @@ -101,7 +101,9 @@ def _is_uniform(arr: npt.NDArray[np.float32]) -> bool: ], cache=True, ) -def _index_of_value_nonuniform(arr: npt.NDArray[np.float32], val: np.float32) -> int: +def _index_of_value_nonuniform( + arr: npt.NDArray[np.float32], val: np.float32 +) -> np.int_: return np.searchsorted((arr[:-1] + arr[1:]) / 2, val) @@ -146,7 +148,6 @@ class ArraySlicer(QtCore.QObject): def __init__(self, xarray_obj: xr.DataArray): super().__init__() - self._obj: xr.DataArray | None = None self.set_array(xarray_obj, validate=True, reset=True) @property @@ -205,29 +206,29 @@ def data_vals_T(self) -> npt.NDArray[np.floating]: # Benchmarks result in 10~20x slower speeds for bottleneck and numbagg compared to # numpy on arm64 mac with Accelerate BLAS. Needs confirmation on intel systems. @functools.cached_property - def nanmax(self) -> np.floating: - return np.nanmax(self._obj.values) + def nanmax(self) -> float: + return float(np.nanmax(self._obj.values)) @functools.cached_property - def nanmin(self) -> np.floating: - return np.nanmin(self._obj.values) + def nanmin(self) -> float: + return float(np.nanmin(self._obj.values)) @functools.cached_property - def absnanmax(self) -> np.floating: + def absnanmax(self) -> float: return max(abs(self.nanmin), abs(self.nanmax)) @functools.cached_property - def absnanmin(self) -> np.floating: + def absnanmin(self) -> float: mn, mx = self.nanmin, self.nanmax if mn * mx <= np.float32(0.0): - return np.float32(0.0) + return 0.0 elif mn < np.float32(0.0): return -mx else: return mn @property - def limits(self) -> tuple[np.floating, np.floating]: + def limits(self) -> tuple[float, float]: """Returns the global minima and maxima of the data.""" return self.nanmin, self.nanmax @@ -265,7 +266,7 @@ def validate_array(data: xr.DataArray) -> xr.DataArray: # if data has kx and ky axis, transpose if "eV" in data.dims: new_dims += ("eV",) - new_dims += tuple(d for d in data.dims if d not in new_dims) + new_dims += tuple(str(d) for d in data.dims if d not in new_dims) data = data.transpose(*new_dims) nonuniform_dims: list[str] = [ @@ -304,13 +305,14 @@ def clear_cache(self): def set_array( self, xarray_obj: xr.DataArray, validate: bool = True, reset: bool = False ) -> None: - del self._obj + if hasattr(self, "_obj"): + del self._obj if validate: self._obj: xr.DataArray = self.validate_array(xarray_obj) else: - self._obj: xr.DataArray = xarray_obj - self._nonuniform_axes: list[str] = [ + self._obj = xarray_obj + self._nonuniform_axes: list[int] = [ i for i, d in enumerate(self._obj.dims) if str(d).endswith("_idx") ] @@ -324,11 +326,11 @@ def set_array( [s // 2 - (1 if s % 2 == 0 else 0) for s in self._obj.shape] ] self._values: list[list[np.float32]] = [ - [c[i] for c, i in zip(self.coords, self._indices[0])] + [c[i] for c, i in zip(self.coords, self._indices[0], strict=True)] ] self.snap_to_data: bool = False - def values_of_dim(self, dim: str) -> npt.NDArray[np.float32]: + def values_of_dim(self, dim: Hashable) -> npt.NDArray[np.float32]: """Fast equivalent of :code:`self._obj[dim].values`. Returns the cached pointer of the underlying coordinate array, achieving a ~80x @@ -353,13 +355,13 @@ def values_of_dim(self, dim: str) -> npt.NDArray[np.float32]: do the trick. """ - return self._obj._coords[dim]._data.array._data + return self._obj._coords[dim]._data.array._data # type: ignore[union-attr] def add_cursor(self, like_cursor: int = -1, update: bool = True) -> None: self._bins.append(list(self.get_bins(like_cursor))) new_ind = self.get_indices(like_cursor) self._indices.append(list(new_ind)) - self._values.append([c[i] for c, i in zip(self.coords, new_ind)]) + self._values.append([c[i] for c, i in zip(self.coords, new_ind, strict=True)]) if update: self.sigCursorCountChanged.emit(self.n_cursors) @@ -580,7 +582,8 @@ def isel_args( ) -> dict[str, slice | int]: axis = sorted(set(range(self._obj.ndim)) - set(disp)) return { - self._obj.dims[ax]: self._bin_slice(cursor, ax, int_if_one) for ax in axis + str(self._obj.dims[ax]): self._bin_slice(cursor, ax, int_if_one) + for ax in axis } def qsel_args(self, cursor: int, disp: Sequence[int]) -> dict: @@ -638,17 +641,17 @@ def isel_code(self, cursor: int, disp: Sequence[int]) -> str: return f".isel({dict_repr})" def xslice(self, cursor: int, disp: Sequence[int]) -> xr.DataArray: - isel_kw: dict[str, slice] = self.isel_args(cursor, disp, int_if_one=False) + isel_kw = self.isel_args(cursor, disp, int_if_one=False) binned_coord_average: dict[str, xr.DataArray] = { - k: self._obj[k][isel_kw[k]].mean() - for k, v in zip(self._obj.dims, self.get_binned(cursor)) + str(k): self._obj[k][isel_kw[str(k)]].mean() + for k, v in zip(self._obj.dims, self.get_binned(cursor), strict=True) if v } return ( - self._obj.isel(**isel_kw) + self._obj.isel(isel_kw) .squeeze() .mean(binned_coord_average.keys()) - .assign_coords(**binned_coord_average) + .assign_coords(binned_coord_average) ) @QtCore.Slot(int, tuple, result=np.ndarray) @@ -673,6 +676,8 @@ def extract_avg_slice( def span_bounds(self, cursor: int, axis: int) -> npt.NDArray[np.float32]: slc = self._bin_slice(cursor, axis) + if isinstance(slc, int): + return self.coords_uniform[axis][slc : slc + 1] lb = max(0, slc.start) ub = min(self._obj.shape[axis] - 1, slc.stop - 1) return self.coords_uniform[axis][[lb, ub]] diff --git a/src/erlab/interactive/kspace.py b/src/erlab/interactive/kspace.py index f2f5ad5d..cd26472c 100644 --- a/src/erlab/interactive/kspace.py +++ b/src/erlab/interactive/kspace.py @@ -24,7 +24,7 @@ class KspaceToolGUI( - *uic.loadUiType(os.path.join(os.path.dirname(__file__), "ktool.ui")) + *uic.loadUiType(os.path.join(os.path.dirname(__file__), "ktool.ui")) # type: ignore[misc] ): def __init__(self): # Start the QApplication if it doesn't exist @@ -155,7 +155,9 @@ def __init__(self, data: xr.DataArray, *, data_name: str | None = None): if data_name is None: try: self._argnames["data"] = varname.argname( - "data", func=self.__init__, vars_only=False + "data", + func=self.__init__, # type: ignore[misc] + vars_only=False, ) except varname.VarnameRetrievingError: self._argnames["data"] = "data" @@ -260,11 +262,11 @@ def show_converted(self): wait_dialog.setLayout(QtWidgets.QVBoxLayout()) wait_dialog.layout().addWidget(QtWidgets.QLabel("Converting...")) wait_dialog.open() - itool = ImageTool( + self._itool = ImageTool( self.data.kspace.convert(bounds=self.bounds, resolution=self.resolution) ) wait_dialog.close() - itool.show() + self._itool.show() def copy_code(self): arg_dict = {} @@ -302,7 +304,10 @@ def copy_code(self): def bounds(self) -> dict[str, tuple[float, float]] | None: if self.bounds_group.isChecked(): return { - k: tuple(self._bound_spins[f"{k}{j}"].value() for j in range(2)) + k: ( + self._bound_spins[f"{k}0"].value(), + self._bound_spins[f"{k}1"].value(), + ) for k in self.data.kspace.momentum_axes } else: @@ -425,5 +430,7 @@ def ktool(data: xr.DataArray, *, data_name: str | None = None) -> KspaceTool: if __name__ == "__main__": - dat = erlab.io.load_hdf5("/Users/khan/2210_ALS_f0008.h5") + from typing import cast + + dat = cast(xr.DataArray, erlab.io.load_hdf5("/Users/khan/2210_ALS_f0008.h5")) win = ktool(dat) diff --git a/src/erlab/interactive/masktool.py b/src/erlab/interactive/masktool.py index 18221878..7bc4a1e7 100644 --- a/src/erlab/interactive/masktool.py +++ b/src/erlab/interactive/masktool.py @@ -1,7 +1,5 @@ -import sys - import pyqtgraph as pg -from pyqtgraph.Qt import QtCore, QtWidgets +from pyqtgraph.Qt import QtCore from erlab.interactive.utilities import AnalysisWindow, ParameterGroup @@ -85,23 +83,3 @@ def update_cursor(self, change): # self.images[0].setImage(self.data.isel({dim_z:self.cursor.widgets["slider"].value()}).values) # self.cursor.values["slider"] - - -if __name__ == "__main__": - import erlab.io - - qapp = QtWidgets.QApplication.instance() - if not qapp: - qapp = QtWidgets.QApplication(sys.argv) - qapp.setStyle("Fusion") - - ds = erlab.io.load_igor_h5( - "/Users/khan/Documents/ERLab/CsV3Sb5/220630_ALS_Kagome_nesting/maps.h5" - ) - map3 = ds["Map3"].rename(phony_dim_0="kx", phony_dim_1="ky", phony_dim_2="eV") - # map6 = ds["Map6"].rename(phony_dim_0="kx", phony_dim_3="ky", phony_dim_4="eV") - ct = masktool(map3) - ct.show() - ct.activateWindow() - ct.raise_() - qapp.exec() diff --git a/src/erlab/interactive/utilities.py b/src/erlab/interactive/utilities.py index 9cbc9789..6296bcda 100644 --- a/src/erlab/interactive/utilities.py +++ b/src/erlab/interactive/utilities.py @@ -4,8 +4,9 @@ import re import sys +import types import warnings -from typing import Any, Literal +from typing import TYPE_CHECKING, Any, Literal, cast import numpy as np import numpy.typing as npt @@ -13,10 +14,12 @@ import pyqtgraph as pg import xarray as xr from qtpy import QtCore, QtGui, QtWidgets -from superqt import QDoubleSlider from erlab.interactive.colors import BetterImageItem, pg_colormap_powernorm +if TYPE_CHECKING: + from collections.abc import Mapping + __all__ = [ "AnalysisWidgetBase", "AnalysisWindow", @@ -251,7 +254,9 @@ def __init__( self._updateWidth() if self.isReadOnly(): - self.lineEdit().setReadOnly(True) + line_edit = self.lineEdit() + if line_edit is not None: + line_edit.setReadOnly(True) self.setButtonSymbols(self.ButtonSymbols.NoButtons) self.setValue(self.value()) @@ -516,6 +521,7 @@ def labelString(self): for k, v in zip( ("0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "-"), ("⁰", "¹", "²", "³", "⁴", "⁵", "⁶", "⁷", "⁸", "⁹", "⁻"), + strict=True, ): units = units.replace(k, v) units = f"10{units}" @@ -565,8 +571,9 @@ def __init__( super().__init__() if spin_kw is None: spin_kw = {} - self.layout = QtWidgets.QHBoxLayout(self) - self.layout.setContentsMargins(0, 0, 0, 0) + layout = QtWidgets.QHBoxLayout(self) + self.setLayout(layout) + layout.setContentsMargins(0, 0, 0, 0) self.param_name = name self._prefix = "" @@ -591,14 +598,15 @@ def __init__( self.spin_ub.setSizePolicy( QtWidgets.QSizePolicy.Policy.Minimum, QtWidgets.QSizePolicy.Policy.Fixed ) - self.check = QtWidgets.QCheckBox(toolTip="Fixed") + self.check = QtWidgets.QCheckBox() + self.check.setToolTip("Fix parameter") if show_label: - self.layout.addWidget(self.label) - self.layout.addWidget(self.spin_value) - self.layout.addWidget(self.spin_lb) - self.layout.addWidget(self.spin_ub) - self.layout.addWidget(self.check) + layout.addWidget(self.label) + layout.addWidget(self.spin_value) + layout.addWidget(self.spin_lb) + layout.addWidget(self.spin_ub) + layout.addWidget(self.check) for spin in (self.spin_value, self.spin_lb, self.spin_ub): spin.valueChanged.connect(lambda: self.sigParamChanged.emit()) @@ -811,31 +819,44 @@ class ParameterGroup(QtWidgets.QGroupBox): """ - VALID_QWTYPE: dict[str, QtWidgets.QWidget] = { - "spin": QtWidgets.QSpinBox, - "dblspin": QtWidgets.QDoubleSpinBox, - "btspin": BetterSpinBox, - "slider": QtWidgets.QSlider, - "dblslider": QDoubleSlider, - "chkbox": QtWidgets.QCheckBox, - "pushbtn": QtWidgets.QPushButton, - "chkpushbtn": QtWidgets.QPushButton, - "combobox": QtWidgets.QComboBox, - "fitparam": FittingParameterWidget, - } # : Dictionary of valid widgets that can be added. + VALID_QWTYPE: Mapping[str, type[QtWidgets.QWidget]] = types.MappingProxyType( + { + "spin": QtWidgets.QSpinBox, + "dblspin": QtWidgets.QDoubleSpinBox, + "btspin": BetterSpinBox, + "slider": QtWidgets.QSlider, + "chkbox": QtWidgets.QCheckBox, + "pushbtn": QtWidgets.QPushButton, + "chkpushbtn": QtWidgets.QPushButton, + "combobox": QtWidgets.QComboBox, + "fitparam": FittingParameterWidget, + } + ) # : Dictionary of valid widgets that can be added. sigParameterChanged: QtCore.SignalInstance = QtCore.Signal(dict) #: :meta private: - def __init__(self, ncols: int = 1, groupbox_kw: dict | None = None, **kwargs): + def __init__( + self, + widgets: dict[str, dict] | None = None, + ncols: int = 1, + groupbox_kw: dict | None = None, + **widgets_kwargs, + ): if groupbox_kw is None: groupbox_kw = {} super().__init__(**groupbox_kw) - self.setLayout(QtWidgets.QGridLayout(self)) + layout = QtWidgets.QGridLayout(self) + self.setLayout(layout) self.labels = [] self.untracked = [] self.widgets: dict[str, QtWidgets.QWidget] = {} + if widgets is not None: + kwargs = widgets + else: + kwargs = widgets_kwargs + j = 0 for i, (k, v) in enumerate(kwargs.items()): if isinstance(v, dict): @@ -859,12 +880,12 @@ def __init__(self, ncols: int = 1, groupbox_kw: dict | None = None, **kwargs): self.labels.append(QtWidgets.QLabel(str(showlabel))) self.labels[i].setBuddy(self.widgets[k]) if showlabel: - self.layout().addWidget(self.labels[i], j // ncols, 2 * (j % ncols)) - self.layout().addWidget( + layout.addWidget(self.labels[i], j // ncols, 2 * (j % ncols)) + layout.addWidget( self.widgets[k], j // ncols, 2 * (j % ncols) + 1, 1, 2 * ind_eff - 1 ) else: - self.layout().addWidget( + layout.addWidget( self.widgets[k], j // ncols, 2 * (j % ncols), 1, 2 * ind_eff ) j += ind_eff @@ -879,7 +900,6 @@ def getParameterWidget( "dblspin", "btspin", "slider", - "dblslider", "chkbox", "pushbtn", "chkpushbtn", @@ -1054,7 +1074,6 @@ def values(self) -> dict[str, float | int | bool]: # "spin": QtWidgets.QSpinBox, # "dblspin": QtWidgets.QDoubleSpinBox, # "slider": QtWidgets.QSlider, - # "dblslider": QDoubleSlider, # "chkbox": QtWidgets.QCheckBox, # "pushbtn": QtWidgets.QPushButton, # "chkpushbtn": QtWidgets.QPushButton, @@ -1134,7 +1153,7 @@ def update_pos(self): self.widgets["y0"].setMaximum(self.widgets["y1"].value()) self.widgets["x1"].setMinimum(self.widgets["x0"].value()) self.widgets["y1"].setMinimum(self.widgets["y0"].value()) - for pos, spin in zip(self.roi_limits, self.roi_spin): + for pos, spin in zip(self.roi_limits, self.roi_spin, strict=True): spin.blockSignals(True) spin.setValue(pos) spin.blockSignals(False) @@ -1142,7 +1161,9 @@ def update_pos(self): def modify_roi(self, x0=None, y0=None, x1=None, y1=None, update=True): lim_new = (x0, y0, x1, y1) lim_old = self.roi_limits - x0, y0, x1, y1 = ((f if f is not None else i) for i, f in zip(lim_old, lim_new)) + x0, y0, x1, y1 = ( + (f if f is not None else i) for i, f in zip(lim_old, lim_new, strict=True) + ) xm, ym, xM, yM = self.roi.maxBounds.getCoords() x0, y0, x1, y1 = max(x0, xm), max(y0, ym), min(x1, xM), min(y1, yM) self.roi.setPos((x0, y0), update=False) @@ -1224,13 +1245,6 @@ def mouseDragEventCustom(ev, axis=None): vb.mouseDragEvent = mouseDragEventCustom # set to modified mouseDragEvent -class PostInitCaller(type(QtWidgets.QMainWindow)): - def __call__(cls, *args, **kwargs): - obj = type.__call__(cls, *args, **kwargs) - obj.__post_init__() - return obj - - class AnalysisWindow(QtWidgets.QMainWindow): def __init__( self, @@ -1318,9 +1332,9 @@ def addParameterGroup(self, *args, **kwargs): self.controls.addWidget(group) return group - def closeEvent(self, event: QtGui.QCloseEvent) -> None: - cb = QtWidgets.QApplication.instance().clipboard() - if cb.text(cb.Mode.Clipboard) != "": + def closeEvent(self, event: QtGui.QCloseEvent | None) -> None: + cb = cast(QtWidgets.QApplication, QtWidgets.QApplication.instance()).clipboard() + if event is not None and cb is not None and cb.text(cb.Mode.Clipboard) != "": pyperclip.copy(cb.text(cb.Mode.Clipboard)) return super().closeEvent(event) @@ -1368,10 +1382,12 @@ def __init__( if link in ("y", "both"): self.axes[i].setYLink(self.axes[0]) - def initialize_layout(self, nax): - self.hists = [pg.HistogramLUTItem() for _ in range(nax)] - self.axes = [pg.PlotItem() for _ in range(nax)] - self.images = [xImageItem(axisOrder="row-major") for _ in range(nax)] + def initialize_layout(self, nax: int): + self.hists: pg.HistogramLUTItem = [pg.HistogramLUTItem() for _ in range(nax)] + self.axes: list[pg.PlotItem] = [pg.PlotItem() for _ in range(nax)] + self.images: list[xImageItem] = [ + xImageItem(axisOrder="row-major") for _ in range(nax) + ] cmap = pg_colormap_powernorm("terrain", 1.0, N=6) for i in range(nax): self.addItem(self.axes[i], *self.get_axis_pos(i)) @@ -1504,7 +1520,7 @@ def refresh_all(self): class DictMenuBar(QtWidgets.QMenuBar): - def __init__(self, parent: QtWidgets.QWidget | None = ..., **kwargs) -> None: + def __init__(self, parent: QtWidgets.QWidget | None = None, **kwargs) -> None: super().__init__(parent) self.menu_dict: dict[str, QtWidgets.QMenu] = {} @@ -1517,7 +1533,7 @@ def __getattribute__(self, __name: str) -> Any: return super().__getattribute__(__name) except AttributeError: try: - out = self.menu_dict[__name] + out: Any = self.menu_dict[__name] except KeyError: out = self.action_dict[__name] warnings.warn( @@ -1596,7 +1612,9 @@ def parse_action(actopts: dict): if __name__ == "__main__": from scipy.ndimage import gaussian_filter # , uniform_filter - qapp = QtWidgets.QApplication.instance() + qapp: QtWidgets.QApplication = cast( + QtWidgets.QApplication, QtWidgets.QApplication.instance() + ) if not qapp: qapp = QtWidgets.QApplication(sys.argv) qapp.setStyle("Fusion") diff --git a/src/erlab/io/__init__.py b/src/erlab/io/__init__.py index 0cd82616..41d7177f 100644 --- a/src/erlab/io/__init__.py +++ b/src/erlab/io/__init__.py @@ -16,6 +16,7 @@ utilities igor exampledata + characterization For a single session, it is very common to use only one type of loader for a single diff --git a/src/erlab/io/characterization/__init__.py b/src/erlab/io/characterization/__init__.py new file mode 100644 index 00000000..b8ec9f36 --- /dev/null +++ b/src/erlab/io/characterization/__init__.py @@ -0,0 +1,14 @@ +"""Data import for characterization experiments. + +.. currentmodule:: erlab.io.characterization + +Modules +======= + +.. autosummary:: + :toctree: + + xrd + resistance + +""" diff --git a/src/erlab/characterization/resistance.py b/src/erlab/io/characterization/resistance.py similarity index 97% rename from src/erlab/characterization/resistance.py rename to src/erlab/io/characterization/resistance.py index 427ddb06..69fdb543 100644 --- a/src/erlab/characterization/resistance.py +++ b/src/erlab/io/characterization/resistance.py @@ -1,4 +1,4 @@ -"""Functions related to analyzing temperature-dependent resistance data. +"""Functions related to loading temperature-dependent resistance data. Currently only supports loading raw data from ``.dat`` and ``.csv`` files output by physics lab III equipment. diff --git a/src/erlab/characterization/xrd.py b/src/erlab/io/characterization/xrd.py similarity index 86% rename from src/erlab/characterization/xrd.py rename to src/erlab/io/characterization/xrd.py index 477e10d3..a142f38d 100644 --- a/src/erlab/characterization/xrd.py +++ b/src/erlab/io/characterization/xrd.py @@ -1,4 +1,4 @@ -"""Functions related to analyzing x-ray diffraction spectra. +"""Functions related to loading x-ray diffraction spectra. Currently only supports loading raw data from igor ``.itx`` files. @@ -57,9 +57,12 @@ def load_xrd_itx(path: str, **kwargs): kwargs.setdefault("encoding", "windows-1252") with open(path, **kwargs) as file: content = file.read() - head, data = re.search( - r"IGOR\nWAVES/O\s(.*?)\nBEGIN\n(.+?)\nEND", content, re.DOTALL - ).groups() + + search = re.search(r"IGOR\nWAVES/O\s(.*?)\nBEGIN\n(.+?)\nEND", content, re.DOTALL) + if search is None: + raise ValueError("Failed to parse .itx file.") + + head, data = search.groups() head = head.split(", ") data = np.array( diff --git a/src/erlab/io/dataloader.py b/src/erlab/io/dataloader.py index 792545f5..273f1749 100644 --- a/src/erlab/io/dataloader.py +++ b/src/erlab/io/dataloader.py @@ -23,7 +23,7 @@ import itertools import os import warnings -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any, ClassVar, Self, cast import joblib import numpy as np @@ -32,7 +32,9 @@ import xarray as xr if TYPE_CHECKING: - from collections.abc import Iterable, Sequence + from collections.abc import Iterable, Mapping + + DataFromSingleFile = xr.DataArray | xr.Dataset | list[xr.DataArray] def _is_uniform(arr: npt.NDArray) -> bool: @@ -40,7 +42,7 @@ def _is_uniform(arr: npt.NDArray) -> bool: return np.allclose(dif, dif[0], rtol=3e-05, atol=3e-05, equal_nan=True) -def _is_monotonic(arr: npt.NDArray) -> bool: +def _is_monotonic(arr: npt.NDArray) -> np.bool_: dif = np.diff(arr) return np.all(dif >= 0) or np.all(dif <= 0) @@ -53,19 +55,26 @@ class ValidationWarning(UserWarning): """This warning is issued when the loaded data fails validation checks.""" +class LoaderNotFoundError(Exception): + """This exception is raised when a loader is not found in the registry.""" + + def __init__(self, key: str): + super().__init__(f"Loader for name or alias {key} not found in the registry") + + class LoaderBase: """Base class for all data loaders.""" - name: str = None + name: str """ Name of the loader. Using a unique and descriptive name is recommended. For easy access, it is recommended to use a name that passes :func:`str.isidentifier`. """ - aliases: list[str] | None = None + aliases: Iterable[str] | None = None """List of alternative names for the loader.""" - name_map: dict[str, str | Iterable[str]] = {} + name_map: ClassVar[dict[str, str | Iterable[str]]] = {} """ Dictionary that maps **new** coordinate or attribute names to **original** coordinate or attribute names. If there are multiple possible names for a single @@ -83,7 +92,7 @@ class LoaderBase: consistency. """ - additional_attrs: dict[str, str | int | float] = {} + additional_attrs: ClassVar[dict[str, str | int | float]] = {} """Additional attributes to be added to the data.""" always_single: bool = True @@ -107,7 +116,7 @@ def name_map_reversed(self) -> dict[str, str]: return self.reverse_mapping(self.name_map) @staticmethod - def reverse_mapping(mapping: dict[str, str | Iterable[str]]) -> dict[str, str]: + def reverse_mapping(mapping: Mapping[str, str | Iterable[str]]) -> dict[str, str]: """Reverse the given mapping dictionary to form a one-to-one mapping. Parameters @@ -268,6 +277,7 @@ def formatter(cls, val: object): ) elif np.issubdtype(type(val), np.floating): + val = cast(np.floating, val) if val.is_integer(): return cls.formatter(np.int64(val)) else: @@ -311,8 +321,8 @@ def get_styler(cls, df: pandas.DataFrame) -> pandas.io.formats.style.Styler: def load( self, - identifier: str | os.PathLike | int | None, - data_dir: str | os.PathLike | None = None, + identifier: str | int, + data_dir: str | None = None, **kwargs, ) -> xr.DataArray | xr.Dataset | list[xr.DataArray]: """Load ARPES data. @@ -400,8 +410,6 @@ def load( if not self.skip_validate: self.validate(data) - data.attrs["data_loader_name"] = str(self.name) - return data def summarize( @@ -467,19 +475,19 @@ def summarize( styled = self.get_styler(df) try: - shell = get_ipython().__class__.__name__ # type: ignore + shell = get_ipython().__class__.__name__ # type: ignore[name-defined] if display and ( shell in ["ZMQInteractiveShell", "TerminalInteractiveShell"] ): - from IPython.display import display + from IPython.display import display # type: ignore[assignment] with pandas.option_context( "display.max_rows", len(df), "display.max_columns", len(df.columns) ): - display(styled) + display(styled) # type: ignore[misc] if importlib.util.find_spec("ipywidgets"): - display(self.isummarize(df=df)) + display(self.isummarize(df=df)) # type: ignore[misc] return None @@ -513,12 +521,10 @@ def isummarize(self, df: pandas.DataFrame | None = None, **kwargs): "ipywidgets and IPython is required for interactive summaries" ) if df is None: - kwargs.setdefault("display", False) - df = self.summarize(**kwargs) + kwargs["display"] = False + df = cast(pandas.DataFrame, self.summarize(**kwargs)) import matplotlib.pyplot as plt - import erlab.plotting.erplot as eplt - from ipywidgets import ( HTML, Button, @@ -532,6 +538,8 @@ def isummarize(self, df: pandas.DataFrame | None = None, **kwargs): ) from ipywidgets.widgets.interaction import show_inline_matplotlib_plots + import erlab.plotting.erplot as eplt + self._temp_data: xr.DataArray | None = None def _format_data_info(series) -> str: @@ -755,7 +763,7 @@ def identify( """ raise NotImplementedError("method must be implemented in the subclass") - def infer_index(self, name: str) -> tuple[int | None, dict | None]: + def infer_index(self, name: str) -> tuple[int | None, dict[str, Any]]: """Infer the index for the given file name. This method takes a file name and tries to infer the scan index from it. If the @@ -802,9 +810,11 @@ def generate_summary(self, data_dir: str | os.PathLike) -> pandas.DataFrame: def combine_multiple( self, - data_list: list[xr.DataArray | xr.Dataset], - coord_dict: dict[str, Sequence], - ) -> xr.DataArray | xr.Dataset | Sequence[xr.DataArray | xr.Dataset]: + data_list: list[xr.DataArray | xr.Dataset | list[xr.DataArray]], + coord_dict: dict[str, Iterable], + ) -> ( + xr.DataArray | xr.Dataset | list[xr.DataArray | xr.Dataset | list[xr.DataArray]] + ): if len(coord_dict) == 0: try: # Try to merge the data without conflicts @@ -814,16 +824,25 @@ def combine_multiple( return data_list else: for i in range(len(data_list)): - data_list[i] = data_list[i].assign_coords( - {k: v[i] for k, v in coord_dict.items()} + if isinstance(data_list[i], list): + data_list[i] = self.combine_multiple(data_list[i], coord_dict={}) + + if not isinstance(data_list[i], list): + data_list[i] = data_list[i].assign_coords( + {k: v[i] for k, v in coord_dict.items()} + ) + try: + return xr.concat( + data_list, + dim=next(iter(coord_dict.keys())), + coords="different", ) - return xr.concat( - data_list, dim=next(iter(coord_dict.keys())), coords="different" - ) + except: # noqa: E722 + return data_list def post_process_general( - self, data: xr.DataArray | xr.Dataset | list[xr.DataArray | xr.Dataset] - ) -> xr.DataArray | xr.Dataset | list[xr.DataArray | xr.Dataset]: + self, data: xr.DataArray | xr.Dataset | list[xr.DataArray] + ) -> xr.DataArray | xr.Dataset | list[xr.DataArray]: if isinstance(data, xr.DataArray): return self.post_process(data) @@ -868,11 +887,15 @@ def process_keys( def post_process(self, data: xr.DataArray) -> xr.DataArray: data = self.process_keys(data) - data = data.assign_attrs(self.additional_attrs) + data = data.assign_attrs( + self.additional_attrs | {"data_loader_name": str(self.name)} + ) return data @classmethod - def validate(cls, data: xr.DataArray | xr.Dataset): + def validate( + cls, data: xr.DataArray | xr.Dataset | list[xr.DataArray | xr.Dataset] + ) -> None: """Validate the input data to ensure it is in the correct format. Checks for the presence of all required coordinates and attributes. If the data @@ -918,7 +941,7 @@ def validate(cls, data: xr.DataArray | xr.Dataset): def load_multiple_parallel( self, file_paths: list[str], n_jobs: int | None = None - ) -> list[xr.DataArray | xr.Dataset]: + ) -> list[xr.DataArray | xr.Dataset | list[xr.DataArray]]: """Load multiple files in parallel. Parameters @@ -958,7 +981,7 @@ class RegistryBase: registry is created and used throughout the application. """ - __instance = None + __instance: RegistryBase | None = None def __new__(cls): if not isinstance(cls.__instance, cls): @@ -966,16 +989,16 @@ def __new__(cls): return cls.__instance @classmethod - def instance(cls) -> LoaderRegistry: + def instance(cls) -> Self: """Returns the registry instance.""" return cls() class LoaderRegistry(RegistryBase): - loaders: dict[str, LoaderBase | type[LoaderBase]] = {} + loaders: ClassVar[dict[str, LoaderBase | type[LoaderBase]]] = {} """Registered loaders \n\n:meta hide-value:""" - alias_mapping: dict[str, str] = {} + alias_mapping: ClassVar[dict[str, str]] = {} """Mapping of aliases to loader names \n\n:meta hide-value:""" current_loader: LoaderBase | None = None @@ -996,15 +1019,18 @@ def register(self, loader_class: type[LoaderBase]): def get(self, key: str) -> LoaderBase: loader_name = self.alias_mapping.get(key) + if loader_name is None: + raise LoaderNotFoundError(key) + loader = self.loaders.get(loader_name) if loader is None: - raise KeyError(f"Loader for {key} not found") + raise LoaderNotFoundError(key) if not isinstance(loader, LoaderBase): # If not an instance, create one - self.loaders[loader_name] = loader() - loader = self.loaders[loader_name] + loader = loader() + self.loaders[loader_name] = loader return loader @@ -1014,10 +1040,10 @@ def __getitem__(self, key: str) -> LoaderBase: def __getattr__(self, key: str) -> LoaderBase: try: return self.get(key) - except KeyError as e: - raise AttributeError(f"Loader for {key} not found") from e + except LoaderNotFoundError as e: + raise AttributeError(str(e)) from e - def set_loader(self, loader: str | LoaderBase): + def set_loader(self, loader: str | LoaderBase | None): """Set the current data loader. All subsequent calls to `load` will use the loader set here. @@ -1094,7 +1120,7 @@ def loader_context( if data_dir is not None: self.set_data_dir(old_data_dir) - def set_data_dir(self, data_dir: str | os.PathLike): + def set_data_dir(self, data_dir: str | os.PathLike | None): """Set the default data directory for the data loader. All subsequent calls to `load` will use the `data_dir` set here unless @@ -1111,7 +1137,7 @@ def set_data_dir(self, data_dir: str | os.PathLike): directly, it will not use the default data directory. """ - if not os.path.isdir(data_dir): + if data_dir is not None and not os.path.isdir(data_dir): raise FileNotFoundError(f"Directory {data_dir} not found") self.default_data_dir = data_dir diff --git a/src/erlab/io/exampledata.py b/src/erlab/io/exampledata.py index 869d1c9b..c7ac81f6 100644 --- a/src/erlab/io/exampledata.py +++ b/src/erlab/io/exampledata.py @@ -58,7 +58,7 @@ def generate_data( Eres: float = 2.0e-3, noise: bool = True, seed: int | None = None, - count: int = 1e8, + count: int = 100000000, ccd_sigma: float = 0.6, ) -> xr.DataArray: """Generate simulated data for a given shape in momentum space. @@ -108,12 +108,12 @@ def generate_data( if isinstance(krange, dict): kx = np.linspace(*krange["kx"], shape[0]) ky = np.linspace(*krange["ky"], shape[1]) - elif not np.iterable(krange): - kx = np.linspace(-krange, krange, shape[0]) - ky = np.linspace(-krange, krange, shape[1]) - else: + elif isinstance(krange, tuple): kx = np.linspace(*krange, shape[0]) ky = np.linspace(*krange, shape[1]) + else: + kx = np.linspace(-krange, krange, shape[0]) + ky = np.linspace(-krange, krange, shape[1]) eV = np.linspace(*Erange, shape[2]) @@ -169,7 +169,7 @@ def generate_data_angles( Eres: float = 10.0e-3, noise: bool = True, seed: int | None = None, - count: int = 1e8, + count: int = 100000000, ccd_sigma: float = 0.6, assign_attributes: bool = False, ) -> xr.DataArray: @@ -228,12 +228,12 @@ def generate_data_angles( if isinstance(angrange, dict): alpha = np.linspace(*angrange["alpha"], shape[0]) beta = np.linspace(*angrange["beta"], shape[1]) - elif not np.iterable(angrange): - alpha = np.linspace(-angrange, angrange, shape[0]) - beta = np.linspace(-angrange, angrange, shape[1]) - else: + elif isinstance(angrange, tuple): alpha = np.linspace(*angrange, shape[0]) beta = np.linspace(*angrange, shape[1]) + else: + alpha = np.linspace(-angrange, angrange, shape[0]) + beta = np.linspace(-angrange, angrange, shape[1]) if not isinstance(configuration, erlab.analysis.kspace.AxesConfiguration): configuration = erlab.analysis.kspace.AxesConfiguration(configuration) @@ -307,7 +307,7 @@ def generate_gold_edge( angres: float = 0.1, edge_coeffs: Sequence[float] = (0.04, 1e-5, -3e-4), background_coeffs: Sequence[float] = (1.0, 0.0, -2e-3), - count: int = 1e6, + count: int = 1000000, noise: bool = True, seed: int | None = None, ccd_sigma: float = 0.6, @@ -384,19 +384,3 @@ def generate_gold_edge( ) return data.assign_attrs(temp_sample=temp) - - -if __name__ == "__main__": - # out = generate_data( - # shape=(201, 202, 203), - # krange=1.4, - # Erange=(-0.45, 0.09), - # temp=30, - # bandshift=-0.2, - # count=1000, - # noise=True, - # ) - out = generate_data_angles() - import erlab.plotting.erplot as eplt - - eplt.itool([out, out.kspace.convert()]) diff --git a/src/erlab/io/igor.py b/src/erlab/io/igor.py index 3e86f610..480aa7ce 100644 --- a/src/erlab/io/igor.py +++ b/src/erlab/io/igor.py @@ -4,6 +4,7 @@ import igor2.binarywave import igor2.packed import igor2.record +from typing import Any import numpy as np import xarray as xr @@ -18,9 +19,9 @@ def _load_experiment_raw( ignore: list[str] | None = None, recursive: bool = False, **kwargs, -) -> xr.Dataset: +) -> dict[str, xr.DataArray]: if folder is None: - folder = [] + split_path: list[Any] = [] if ignore is None: ignore = [] @@ -31,13 +32,13 @@ def _load_experiment_raw( except ValueError: continue - waves = {} + waves: dict[str, xr.DataArray] = {} if isinstance(folder, str): - folder = folder.split("/") - folder = [n.encode() for n in folder] + split_path = folder.split("/") + split_path = [n.encode() for n in split_path] expt = expt["root"] - for dirname in folder: + for dirname in split_path: expt = expt[dirname] def unpack_folders(expt): @@ -216,7 +217,7 @@ def get_dim_name(index): dims = [get_dim_name(i) for i in range(_MAXDIM)] coords = { dims[i]: np.linspace(b, b + a * (c - 1), c) - for i, (a, b, c) in enumerate(zip(sfA, sfB, shape)) + for i, (a, b, c) in enumerate(zip(sfA, sfB, shape, strict=True)) if c != 0 } diff --git a/src/erlab/io/plugins/da30.py b/src/erlab/io/plugins/da30.py index 96d0a3e8..8efe41dd 100644 --- a/src/erlab/io/plugins/da30.py +++ b/src/erlab/io/plugins/da30.py @@ -7,7 +7,7 @@ import os import tempfile import zipfile - +from typing import ClassVar import numpy as np import xarray as xr @@ -16,19 +16,18 @@ class DA30Loader(LoaderBase): - name: str = "da30" - aliases: list[str] = ["DA30"] + name = "da30" + aliases = ("DA30",) - name_map: dict[str, str] = { + name_map: ClassVar[dict] = { "eV": ["Kinetic Energy [eV]", "Energy [eV]"], "alpha": ["Y-Scale [deg]", "Thetax [deg]"], "beta": ["Thetay [deg]"], "hv": ["BL Energy", "Excitation Energy"], } - coordinate_attrs: tuple[str, ...] = () - additional_attrs: dict[str, str | int | float] = {} - always_single: bool = True - skip_validate: bool = True + additional_attrs: ClassVar[dict] = {} + always_single = True + skip_validate = True def load_single(self, file_path: str | os.PathLike) -> xr.DataArray: ext = os.path.splitext(file_path)[-1] diff --git a/src/erlab/io/plugins/kriss.py b/src/erlab/io/plugins/kriss.py index 4f8b230f..4abfd68d 100644 --- a/src/erlab/io/plugins/kriss.py +++ b/src/erlab/io/plugins/kriss.py @@ -3,19 +3,20 @@ import os import re from collections.abc import Iterable +from typing import ClassVar import erlab.io.utilities from erlab.io.plugins.da30 import DA30Loader class KRISSLoader(DA30Loader): - name: str = "kriss" + name = "kriss" - aliases: list[str] = ["KRISS"] + aliases = ("KRISS",) - coordinate_attrs: tuple[str, ...] = ("beta", "chi", "xi", "hv", "x", "y", "z") + coordinate_attrs = ("beta", "chi", "xi", "hv", "x", "y", "z") - additional_attrs: dict[str, str | int | float] = {"configuration": 4} + additional_attrs: ClassVar[dict] = {"configuration": 4} @property def name_map(self): diff --git a/src/erlab/io/plugins/merlin.py b/src/erlab/io/plugins/merlin.py index 52d9f66c..82909a50 100644 --- a/src/erlab/io/plugins/merlin.py +++ b/src/erlab/io/plugins/merlin.py @@ -4,6 +4,7 @@ import glob import os import re +from typing import Any, ClassVar import numpy as np import numpy.typing as npt @@ -16,10 +17,11 @@ class BL403Loader(LoaderBase): - name: str = "merlin" - aliases: list[str] = ["ALS_BL4", "als_bl4", "BL403", "bl403"] + name = "merlin" - name_map: dict[str, str | list[str]] = { + aliases = ("ALS_BL4", "als_bl4", "BL403", "bl403") + + name_map: ClassVar[dict] = { "alpha": "deg", "beta": ["Polar", "Polar Compens"], "delta": "Azimuth", @@ -32,7 +34,7 @@ class BL403Loader(LoaderBase): "temp_sample": "Temperature Sensor B", "mesh_current": "Mesh Current", } - coordinate_attrs: tuple[str, ...] = ( + coordinate_attrs = ( "beta", "delta", "xi", @@ -43,11 +45,11 @@ class BL403Loader(LoaderBase): "polarization", "mesh_current", ) - additional_attrs: dict[str, str | int | float] = { + additional_attrs: ClassVar[dict] = { "configuration": 1, "sample_workfunction": 4.44, } - always_single: bool = False + always_single = False def load_single(self, file_path: str | os.PathLike) -> xr.DataArray: if os.path.splitext(file_path)[1] == ".ibw": @@ -59,9 +61,7 @@ def load_single(self, file_path: str | os.PathLike) -> xr.DataArray: return self.process_keys(data) - def identify( - self, num: int, data_dir: str | os.PathLike - ) -> tuple[list[str], dict[str, npt.NDArray[np.float64]]]: + def identify(self, num: int, data_dir: str | os.PathLike): coord_dict: dict[str, npt.NDArray[np.float64]] = {} # Look for scans @@ -74,9 +74,12 @@ def identify( files = glob.glob(f"*_{str(num).zfill(3)}_R*.pxt", root_dir=data_dir) files.sort() elif len(files) > 1: - prefix: str = re.match( + match_prefix = re.match( r"(.*?)_" + str(num).zfill(3) + r"(?:_S\d{3})?.pxt", files[0] - ).group(1) + ) + if match_prefix is None: + raise RuntimeError(f"Failed to match prefix in {files[0]}") + prefix: str = match_prefix.group(1) motor_file = os.path.join( data_dir, f"{prefix}_{str(num).zfill(3)}_Motor_Pos.txt" @@ -104,16 +107,20 @@ def identify( return files, coord_dict - def infer_index(self, name: str) -> tuple[int | None, dict]: + def infer_index(self, name: str) -> tuple[int | None, dict[str, Any]]: try: - scan_num: str = re.match(r".*?(\d{3})(?:_S\d{3})?", name).group(1) - except (AttributeError, IndexError): - return None, None + match_scan = re.match(r".*?(\d{3})(?:_S\d{3})?", name) + if match_scan is None: + return None, {} + + scan_num: str = match_scan.group(1) + except IndexError: + return None, {} if scan_num.isdigit(): return int(scan_num), {} else: - return None, None + return None, {} def post_process(self, data: xr.DataArray) -> xr.DataArray: data = super().post_process(data) diff --git a/src/erlab/io/plugins/ssrl52.py b/src/erlab/io/plugins/ssrl52.py index a13acc0f..6b832d29 100644 --- a/src/erlab/io/plugins/ssrl52.py +++ b/src/erlab/io/plugins/ssrl52.py @@ -3,10 +3,10 @@ import datetime import os import re +from typing import ClassVar import h5netcdf import numpy as np -import numpy.typing as npt import pandas as pd import xarray as xr @@ -15,10 +15,10 @@ class SSRL52Loader(LoaderBase): - name: str = "ssrl" - aliases: list[str] = ["ssrl52", "bl5-2"] + name = "ssrl" + aliases = ("ssrl52", "bl5-2") - name_map: dict[str, str] = { + name_map: ClassVar[dict] = { "eV": "Kinetic Energy", "alpha": "ThetaX", "beta": ["ThetaY", "YDeflection", "DeflectionY"], @@ -32,17 +32,10 @@ class SSRL52Loader(LoaderBase): "temp_sample": ["TB", "sample_stage_temperature"], "sample_workfunction": "WorkFunction", } - coordinate_attrs: tuple[str, ...] = ( - "beta", - "delta", - "chi", - "xi", - "hv", - "x", - "y", - "z", - ) - additional_attrs: dict[str, str | int | float] = { + + coordinate_attrs = ("beta", "delta", "chi", "xi", "hv", "x", "y", "z") + + additional_attrs: ClassVar[dict] = { "configuration": 3, "sample_workfunction": 4.5, } @@ -123,7 +116,7 @@ def identify( num: int, data_dir: str | os.PathLike, zap: bool = False, - ) -> tuple[list[str], dict[str, npt.NDArray[np.float64]]]: + ): if zap: target_files = erlab.io.utilities.get_files( data_dir, extensions=(".h5",), contains="zap" diff --git a/src/erlab/io/utilities.py b/src/erlab/io/utilities.py index b9437074..502f463c 100644 --- a/src/erlab/io/utilities.py +++ b/src/erlab/io/utilities.py @@ -204,7 +204,7 @@ def save_as_hdf5( # IGORWaveScaling order: chunk row column layer scaling = [[1, 0]] for i in range(data.ndim): - coord: npt.NDArray = data[data.dims[i]].values + coord: npt.NDArray = np.asarray(data[data.dims[i]].values) delta = coord[1] - coord[0] scaling.append([delta, coord[0]]) if data.ndim == 4: diff --git a/src/erlab/plotting/__init__.py b/src/erlab/plotting/__init__.py index e8c96789..366374b9 100644 --- a/src/erlab/plotting/__init__.py +++ b/src/erlab/plotting/__init__.py @@ -62,6 +62,10 @@ def load_igor_ct(fname: str, name: str) -> None: """ file = pkgutil.get_data(__package__, "IgorCT/" + fname) + + if file is None: + raise FileNotFoundError(f"Could not find file {fname}") + if fname.endswith(".txt"): values = np.genfromtxt(io.StringIO(file.decode())) elif fname.endswith(".ibw"): diff --git a/src/erlab/plotting/annotations.py b/src/erlab/plotting/annotations.py index 6d9d1137..f7864318 100644 --- a/src/erlab/plotting/annotations.py +++ b/src/erlab/plotting/annotations.py @@ -23,9 +23,13 @@ import io import re -from typing import TYPE_CHECKING, Literal +from typing import TYPE_CHECKING, Any, Literal, cast import matplotlib +import matplotlib.backends.backend_pdf +import matplotlib.backends.backend_svg +import matplotlib.figure +import matplotlib.mathtext import matplotlib.pyplot as plt import matplotlib.ticker import matplotlib.transforms as mtransforms @@ -264,6 +268,7 @@ def copy_mathtext( rcparams = {} parser = matplotlib.mathtext.MathTextParser("path") width, height, depth, _, _ = parser.parse(s, dpi=72, prop=fontproperties) + fig = matplotlib.figure.Figure(figsize=(width / 72, height / 72)) fig.patch.set_facecolor("none") fig.text(0, depth / height, s, fontproperties=fontproperties) @@ -285,11 +290,11 @@ def copy_mathtext( rcparams.setdefault("svg.fonttype", "path" if outline else "none") rcparams.setdefault("svg.image_inline", True) with plt.rc_context(rcparams): - fig.canvas.print_svg(buffer) + fig.canvas.print_svg(buffer) # type: ignore[attr-defined] else: rcparams.setdefault("pdf.fonttype", 3 if outline else 42) with plt.rc_context(rcparams): - fig.canvas.print_pdf(buffer) + fig.canvas.print_pdf(buffer) # type: ignore[attr-defined] pyperclip.copy(buffer.getvalue().decode("utf-8")) @@ -308,7 +313,7 @@ def fancy_labels(ax=None, deg2rad=False): def label_subplot_properties( - axes: matplotlib.axes.Axes | Sequence[matplotlib.axes.Axes], + axes: matplotlib.axes.Axes | Iterable[matplotlib.axes.Axes], values: dict, decimals: int | None = None, si: int = 0, @@ -352,7 +357,7 @@ def label_subplot_properties( kwargs.setdefault("suffix", "") kwargs.setdefault("loc", "upper right") - strlist = [] + strlist: Any = [] for k, v in values.items(): if not isinstance(v, tuple | list | np.ndarray): v = [v] @@ -364,14 +369,14 @@ def label_subplot_properties( for val in v ] ) - strlist = list(zip(*strlist)) + strlist = list(zip(*strlist, strict=True)) strlist = ["\n".join(strlist[i]) for i in range(len(strlist))] label_subplots(axes, strlist, order=order, **kwargs) def label_subplots( - axes: matplotlib.axes.Axes | Sequence[matplotlib.axes.Axes], - values: Sequence[int | str] | None = None, + axes: matplotlib.axes.Axes | Iterable[matplotlib.axes.Axes], + values: Iterable[int | str] | None = None, startfrom: int = 1, order: Literal["C", "F", "A", "K"] = "C", loc: Literal[ @@ -467,10 +472,12 @@ def label_subplots( axlist = np.array(axes, dtype=object).flatten(order=order) if values is None: - values = np.array([i + startfrom for i in range(len(axlist))], dtype=np.int64) + value_arr = np.array( + [i + startfrom for i in range(len(axlist))], dtype=np.int64 + ) else: - values = np.array(values).flatten(order=order) - if not (axlist.size == values.size): + value_arr = np.array(values).flatten(order=order) + if not (axlist.size == value_arr.size): raise IndexError( "The number of given values must match the number of given axes." ) @@ -479,16 +486,14 @@ def label_subplots( bbox_to_anchor = axlist[i].bbox if fontsize is None: if isinstance(axlist[i], matplotlib.figure.Figure): - fs = "large" + fontsize = "large" else: - fs = "medium" - else: - fs = fontsize + fontsize = "medium" bbox_transform = matplotlib.transforms.ScaledTranslation( offset[0] / 72, offset[1] / 72, axlist[i].get_figure().dpi_scale_trans ) - label_str = _alph_label(values[i], prefix, suffix, numeric, capital) + label_str = _alph_label(value_arr[i], prefix, suffix, numeric, capital) with plt.rc_context({"text.color": axes_textcolor(axlist[i])}): at = matplotlib.offsetbox.AnchoredText( label_str, @@ -496,7 +501,7 @@ def label_subplots( frameon=False, pad=0, borderpad=0.5, - prop=dict(fontsize=fs, **kwargs), + prop=dict(fontsize=fontsize, **kwargs), bbox_to_anchor=bbox_to_anchor, bbox_transform=bbox_transform, clip_on=False, @@ -587,16 +592,18 @@ def label_subplots_nature( axlist = np.array(axes, dtype=object).flatten(order=order) if values is None: - values = np.array([i + startfrom for i in range(len(axlist))], dtype=np.int64) + value_arr = np.array( + [i + startfrom for i in range(len(axlist))], dtype=np.int64 + ) else: - values = np.array(values).flatten(order=order) - if not (axlist.size == values.size): + value_arr = np.array(values).flatten(order=order) + if not (axlist.size == value_arr.size): raise IndexError( "The number of given values must match the number of given axes." ) for i in range(len(axlist)): - label_str = _alph_label(values[i], prefix, suffix, numeric, capital) + label_str = _alph_label(value_arr[i], prefix, suffix, numeric, capital) trans = matplotlib.transforms.ScaledTranslation( offset[0] / 72, offset[1] / 72, axlist[i].get_figure().dpi_scale_trans ) @@ -624,7 +631,7 @@ def mark_points( literal: bool = False, roman: bool = True, bar: bool = False, - ax: matplotlib.axes.Axes | Iterable[matplotlib.axes.Axes] = None, + ax: matplotlib.axes.Axes | Iterable[matplotlib.axes.Axes] | None = None, **kwargs, ): """Mark points above the horizontal axis. @@ -654,23 +661,32 @@ def mark_points( """ if ax is None: ax = plt.gca() + if np.iterable(ax): for a in np.asarray(ax, dtype=object).flatten(): mark_points(points, labels, y, pad, literal, roman, bar, a, **kwargs) else: + ax = cast(matplotlib.axes.Axes, ax) # to appease mypy + fig = ax.get_figure() + + if fig is None: + raise ValueError("Given axes does not belong to a figure") + for k, v in {"ha": "center", "va": "baseline", "fontsize": "small"}.items(): kwargs.setdefault(k, v) + if not np.iterable(y): - y = [y] * len(points) + y = [y] * len(points) # type: ignore[list-item] + with plt.rc_context({"font.family": "serif"}): - for xi, yi, label in zip(points, y, labels): + for xi, yi, label in zip(points, y, labels, strict=True): ax.text( xi, yi, label if literal else parse_point_labels(label, roman, bar), transform=ax.transData + mtransforms.ScaledTranslation( - pad[0] / 72, pad[1] / 72, ax.figure.dpi_scale_trans + pad[0] / 72, pad[1] / 72, fig.dpi_scale_trans ), **kwargs, ) @@ -682,7 +698,7 @@ def mark_points_outside( axis: Literal["x", "y"] = "x", roman: bool = True, bar: bool = False, - ax: matplotlib.axes.Axes | Iterable[matplotlib.axes.Axes] = None, + ax: matplotlib.axes.Axes | Iterable[matplotlib.axes.Axes] | None = None, ): """Mark points above the horizontal axis. @@ -712,6 +728,8 @@ def mark_points_outside( for a in np.asarray(ax, dtype=object).flatten(): mark_points_outside(points, labels, axis, roman, bar, a) else: + ax = cast(matplotlib.axes.Axes, ax) # to appease mypy + if axis == "x": label_ax = ax.twiny() label_ax.set_xlim(ax.get_xlim()) @@ -775,7 +793,7 @@ def plot_hv_text_right(ax, val, x=1 - 0.025, y=0.975, **kwargs): ) -def property_label(key, value, decimals=None, si=0, name=None, unit=None): +def property_label(key, value, decimals=None, si=0, name=None, unit=None) -> str: if name == "": delim = "" else: @@ -883,7 +901,7 @@ def scale_units( def set_titles(axes, labels, order="C", **kwargs): axlist = np.array(axes, dtype=object).flatten(order=order) labels = np.asarray(labels) - for ax, label in zip(axlist.flat, labels.flat): + for ax, label in zip(axlist.flat, labels.flat, strict=True): ax.set_title(label, **kwargs) @@ -892,7 +910,7 @@ def set_xlabels(axes, labels, order="C", **kwargs): if isinstance(labels, str): labels = [labels] * len(axlist) labels = np.asarray(labels) - for ax, label in zip(axlist.flat, labels.flat): + for ax, label in zip(axlist.flat, labels.flat, strict=True): ax.set_xlabel(label, **kwargs) @@ -901,7 +919,7 @@ def set_ylabels(axes, labels, order="C", **kwargs): if isinstance(labels, str): labels = [labels] * len(axlist) labels = np.asarray(labels) - for ax, label in zip(axlist.flat, labels.flat): + for ax, label in zip(axlist.flat, labels.flat, strict=True): ax.set_ylabel(label, **kwargs) diff --git a/src/erlab/plotting/atoms.py b/src/erlab/plotting/atoms.py index 92df4320..b187d959 100644 --- a/src/erlab/plotting/atoms.py +++ b/src/erlab/plotting/atoms.py @@ -9,17 +9,18 @@ import contextlib import functools import itertools -from collections.abc import Callable, Sequence -from typing import Literal +from collections.abc import Callable, Iterable, Mapping, Sequence +from typing import Literal, cast import matplotlib.collections -import matplotlib.colors as mcolors +import matplotlib.colors import matplotlib.pyplot as plt import mpl_toolkits.mplot3d import mpl_toolkits.mplot3d.art3d import mpl_toolkits.mplot3d.proj3d import numpy as np import numpy.typing as npt +from matplotlib.typing import ColorType __all__ = ["Atom3DCollection", "Bond3DCollection", "CrystalProperty"] @@ -78,7 +79,9 @@ def projected_length_pos(ax: mpl_toolkits.mplot3d.Axes3D, length, position): return np.asarray( [ projected_length_pos(ax, d, p) - for d, p in zip(np.asarray(length).flat, np.asarray(position)) + for d, p in zip( + np.asarray(length).flat, np.asarray(position), strict=True + ) ] ) rc = np.asarray(position).reshape(-1, 1) @@ -100,7 +103,7 @@ def _zalpha(colors, zs): return np.zeros((0, 4)) norm = plt.Normalize(min(zs), max(zs)) sats = 1 - norm(zs) * 0.7 - rgba = np.broadcast_to(mcolors.to_rgba_array(colors), (len(zs), 4)) + rgba = np.broadcast_to(matplotlib.colors.to_rgba_array(colors), (len(zs), 4)) rgba = rgba.T * sats rgba += 1 - sats rgba = rgba.T @@ -131,7 +134,7 @@ def _maybe_depth_shade_and_sort_colors(self, color_array): ) if len(color_array) > 1: color_array = color_array[self._z_markers_idx] - return mcolors.to_rgba_array(color_array, self._alpha) + return matplotlib.colors.to_rgba_array(color_array, self._alpha) def set_sizes(self, sizes: np.ndarray, dpi: float = 72.0): super().set_sizes(sizes, dpi) @@ -240,15 +243,15 @@ def draw(self, renderer): class CrystalProperty: def __init__( self, - atom_pos: dict[ - str, npt.NDArray[np.float64] | Sequence[npt.NDArray[np.float64]] + atom_pos: Mapping[ + str, Iterable[float | np.floating | npt.NDArray[np.floating]] ], avec: npt.NDArray[np.float64], - offset: npt.NDArray[np.float64] | Sequence[float] = (0.0, 0.0, 0.0), - radii: Sequence[float] | None = None, - colors: Sequence[str | tuple[float, ...]] | None = None, + offset: Iterable[float] = (0.0, 0.0, 0.0), + radii: Iterable[float] | None = None, + colors: Iterable[ColorType] | None = None, repeat: tuple[int, int, int] = (1, 1, 1), - bounds: dict[Literal["x", "y", "z"], tuple[float, float]] | None = None, + bounds: Mapping[Literal["x", "y", "z"], tuple[float, float]] | None = None, mask: Callable | None = None, r_factor: float = 0.4, ): @@ -296,16 +299,18 @@ def __init__( if radii is None: radii = [1.0] * len(self.atoms) - self.atom_radii: dict[str, float] = dict(zip(self.atoms, radii)) + self.atom_radii: dict[str, float] = dict(zip(self.atoms, radii, strict=True)) if colors is None: colors = [f"C{i}" for i in range(len(self.atoms))] + self.atom_color: dict[str, str] = { - k: mcolors.to_hex(v) for k, v in zip(self.atoms, colors) + k: matplotlib.colors.to_hex(v) + for k, v in zip(self.atoms, colors, strict=True) } self.repeat: tuple[int, int, int] = repeat - self._bounds: dict[Literal["x", "y", "z"], tuple[float, float]] = ( + self._bounds: Mapping[Literal["x", "y", "z"], tuple[float, float]] = ( {} if bounds is None else bounds ) self.mask: Callable | None = mask @@ -326,9 +331,14 @@ def from_fractional( *args, **kwargs, ): - atom_pos = {} + atom_pos: dict[str, list[npt.NDArray[np.float64]]] = {} for k, v in frac_pos.items(): - atom_pos[k] = [x[0] * avec[0] + x[1] * avec[1] + x[2] * avec[2] for x in v] + atom_pos[k] = [ + np.asarray( + x[0] * avec[0] + x[1] * avec[1] + x[2] * avec[2], dtype=np.float64 + ) + for x in v + ] return cls(atom_pos, avec, *args, **kwargs) @property @@ -336,7 +346,7 @@ def bounds(self) -> list[tuple[float, float]]: bound_list = [] for dim in ("x", "y", "z"): try: - bound_list.append(self._bounds[dim]) + bound_list.append(self._bounds[cast(Literal["x", "y", "z"], dim)]) except KeyError: bound_list.append((-np.inf, np.inf)) return bound_list @@ -386,7 +396,7 @@ def atom_pos(self) -> dict[str, npt.NDArray[np.float64]]: return masked_atom_pos @property - def _color_array(self) -> tuple[npt.NDArray[np.str_]]: + def _color_array(self) -> npt.NDArray[np.str_]: return np.asarray( [ self.atom_color[k] @@ -396,7 +406,7 @@ def _color_array(self) -> tuple[npt.NDArray[np.str_]]: ) @property - def _size_array(self) -> tuple[npt.NDArray[np.float64]]: + def _size_array(self) -> npt.NDArray[np.float64]: return np.asarray( [ self.atom_radii[k] @@ -464,6 +474,7 @@ def plot( if ax is None: ax = plt.gcf().add_subplot(projection="3d") + ax = cast(mpl_toolkits.mplot3d.Axes3D, ax) if clean_axes: ax.set_facecolor("none") diff --git a/src/erlab/plotting/bz.py b/src/erlab/plotting/bz.py index 5ffd364c..0031bab7 100644 --- a/src/erlab/plotting/bz.py +++ b/src/erlab/plotting/bz.py @@ -22,7 +22,7 @@ def get_bz_edge( basis: npt.NDArray[np.float64], reciprocal: bool = True, - extend: tuple[int, int, int] | tuple[int, int] | None = None, + extend: tuple[int, ...] | None = None, ) -> tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]: """Calculates the edge of the first Brillouin zone (BZ) from lattice vectors. @@ -47,18 +47,21 @@ def get_bz_edge( Vertices of the BZ. """ - if not (basis.shape == (2, 2) or basis.shape == (3, 3)): + if basis.shape == (2, 2): + ndim = 2 + elif basis.shape == (3, 3): + ndim = 3 + else: raise ValueError("Shape of `basis` must be (N, N) where N = 2 or 3.") + if not reciprocal: basis = 2 * np.pi * np.linalg.inv(basis).T - ndim = basis.shape[-1] - if extend is None: extend = (1,) * ndim points = ( - np.tensordot(basis, np.mgrid[[slice(-1, 2) for _ in range(ndim)]], axes=[0, 0]) + np.tensordot(basis, np.mgrid[[slice(-1, 2) for _ in range(ndim)]], axes=(0, 0)) .reshape((ndim, 3**ndim)) .T ) @@ -71,7 +74,7 @@ def get_bz_edge( lines = [] vertices = [] - for pointidx, simplex in zip(vor.ridge_points, vor.ridge_vertices): + for pointidx, simplex in zip(vor.ridge_points, vor.ridge_vertices, strict=True): simplex = np.asarray(simplex) if zero_ind in pointidx: # If the origin is included in the ridge, add the vertices @@ -79,8 +82,8 @@ def get_bz_edge( vertices.append(vor.vertices[simplex]) # Remove duplicates - lines_new = [] - vertices_new = [] + lines_new: list[npt.NDArray] = [] + vertices_new: list[npt.NDArray] = [] for line in lines: for i in range(line.shape[0] - 1): @@ -94,8 +97,8 @@ def get_bz_edge( if not any(np.allclose(v, vn) for vn in vertices_new): vertices_new.append(v) - lines = np.asarray(lines_new) - vertices = np.asarray(vertices_new) + lines_arr = np.asarray(lines_new) + vertices_arr = np.asarray(vertices_new) # Extend the BZ additional_lines = [] @@ -103,12 +106,12 @@ def get_bz_edge( for vals in itertools.product(*[range(-n + 1, n) for n in extend]): if vals != (0,) * ndim: displacement = np.dot(vals, basis) - additional_lines.append(lines + displacement) - additional_verts.append(vertices + displacement) - lines = np.r_[lines, *additional_lines] - vertices = np.r_[vertices, *additional_verts] + additional_lines.append(lines_arr + displacement) + additional_verts.append(vertices_arr + displacement) + lines_arr = np.r_[lines_arr, *additional_lines] + vertices_arr = np.r_[vertices_arr, *additional_verts] - return lines, vertices + return lines_arr, vertices_arr def plot_hex_bz( diff --git a/src/erlab/plotting/colors.py b/src/erlab/plotting/colors.py index 2ee6c11d..7035d4f7 100644 --- a/src/erlab/plotting/colors.py +++ b/src/erlab/plotting/colors.py @@ -47,18 +47,20 @@ from collections.abc import Iterable, Sequence from numbers import Number -from typing import Any, Literal +from typing import Any, Literal, cast import matplotlib import matplotlib.axes import matplotlib.cm import matplotlib.collections +import matplotlib.colorbar import matplotlib.colors import matplotlib.image import matplotlib.pyplot as plt import matplotlib.transforms import numpy as np import numpy.typing as npt +from matplotlib.typing import ColorType class InversePowerNorm(matplotlib.colors.Normalize): @@ -469,7 +471,8 @@ def get_mappable( image_only Only consider images as a valid mappable, by default `False`. silent - If `False`, raises a `RuntimeError`. If `True`, silently returns `None`. + If `False`, raises a `RuntimeError` when no mappable is found. If `True`, + silently returns `None`. Returns ------- @@ -478,7 +481,7 @@ def get_mappable( """ if not image_only: try: - mappable = ax.collections[-1] + mappable: Any = ax.collections[-1] except (IndexError, AttributeError): mappable = None @@ -488,13 +491,14 @@ def get_mappable( except (IndexError, AttributeError): mappable = None - if not silent and mappable is None: - raise RuntimeError( - "No mappable was found to use for colorbar " - "creation. First define a mappable such as " - "an image (with imshow) or a contour set (" - "with contourf)." - ) + if mappable is None: + if not silent: + raise RuntimeError( + "No mappable was found to use for colorbar " + "creation. First define a mappable such as " + "an image (with imshow) or a contour set (" + "with contourf)." + ) return mappable @@ -517,21 +521,29 @@ def unify_clim( If `True`, only consider mappables that are images. Default is `False`. """ + vmn: float | None + vmx: float | None + if target is None: - vmn, vmx = [], [] + vmn_list, vmx_list = [], [] for ax in axes.flat: - mappable = get_mappable(ax, image_only=image_only) - vmn.append(mappable.norm.vmin) - vmx.append(mappable.norm.vmax) - vmn, vmx = min(vmn), max(vmx) - + mappable = get_mappable(ax, image_only=image_only, silent=True) + if mappable is not None: + if mappable.norm.vmin is not None: + vmn_list.append(mappable.norm.vmin) + if mappable.norm.vmax is not None: + vmx_list.append(mappable.norm.vmax) + vmn, vmx = min(vmn_list), max(vmx_list) else: - mappable = get_mappable(target, image_only=image_only) - vmn, vmx = mappable.norm.vmin, mappable.norm.vmax + mappable = get_mappable(target, image_only=image_only, silent=True) + if mappable is not None: + vmn, vmx = mappable.norm.vmin, mappable.norm.vmax + # Apply color limits for ax in axes.flat: - mappable = get_mappable(ax, image_only=image_only) - mappable.norm.vmin, mappable.norm.vmax = vmn, vmx + mappable = get_mappable(ax, image_only=image_only, silent=True) + if mappable is not None: + mappable.norm.vmin, mappable.norm.vmax = vmn, vmx def proportional_colorbar( @@ -597,7 +609,9 @@ def proportional_colorbar( ax = plt.gca() if mappable is None: mappable = get_mappable(ax) - elif isinstance(ax, np.ndarray): + elif isinstance(ax, Iterable): + if not isinstance(ax, np.ndarray): + ax = np.array(ax, dtype=object) i = 0 while mappable is None and i < len(ax.flat): mappable = get_mappable(ax.flatten()[i], silent=(i != (len(ax.flat) - 1))) @@ -605,8 +619,13 @@ def proportional_colorbar( elif mappable is None: mappable = get_mappable(ax) + if mappable is None: + raise RuntimeError("No mappable was found to use for colorbar creation") + if mappable.colorbar is None: plt.colorbar(mappable=mappable, cax=cax, ax=ax, **kwargs) + mappable.colorbar = cast(matplotlib.colorbar.Colorbar, mappable.colorbar) + ticks = mappable.colorbar.get_ticks() if cax is None: mappable.colorbar.remove() @@ -718,6 +737,8 @@ def _ez_inset( **kwargs, ) -> matplotlib.axes.Axes: fig = parent_axes.get_figure() + if fig is None: + raise RuntimeError("Parent axes is not attached to a figure") locator = InsetAxesLocator(parent_axes, width, height, pad, loc) ax_ = fig.add_axes(locator(parent_axes, None).bounds, **kwargs) ax_.set_axes_locator(locator) @@ -808,7 +829,7 @@ def _gen_cax(ax, width=4.0, aspect=7.0, pad=3.0, horiz=False, **kwargs): # TODO: fix colorbar size properly def nice_colorbar( - ax: matplotlib.axes.Axes | None = None, + ax: matplotlib.axes.Axes | Iterable[matplotlib.axes.Axes] | None = None, mappable: matplotlib.cm.ScalarMappable | None = None, width: float = 5.0, aspect: float = 5.0, @@ -874,7 +895,9 @@ def nice_colorbar( ) else: - if np.iterable(ax): + if isinstance(ax, Iterable): + if not isinstance(ax, np.ndarray): + ax = np.array(ax, dtype=object) bbox = matplotlib.transforms.Bbox.union( [ x.get_window_extent().transformed( @@ -884,9 +907,12 @@ def nice_colorbar( ] ) else: - bbox = ax.get_window_extent().transformed( - ax.figure.dpi_scale_trans.inverted() - ) + fig = ax.get_figure() + + if fig is None: + raise RuntimeError("Axes is not attached to a figure") + + bbox = ax.get_window_extent().transformed(fig.dpi_scale_trans.inverted()) if orientation == "horizontal": kwargs["anchor"] = (1, 1) @@ -951,10 +977,21 @@ def flatten_transparency(rgba: npt.NDArray, background: Sequence[float] | None = return rgb.reshape(original_shape[:-1] + (3,)) +def _get_segment_for_color( + cmap: matplotlib.colors.LinearSegmentedColormap, + color: Literal["red", "green", "blue", "alpha"], +) -> Any: + if hasattr(cmap, "_segmentdata"): + if color in cmap._segmentdata: + return cmap._segmentdata[color] + return None + + def _is_segment_iterable(cmap: matplotlib.colors.Colormap) -> bool: if not isinstance(cmap, matplotlib.colors.LinearSegmentedColormap): return False - if any(callable(cmap._segmentdata[c]) for c in ["red", "green", "blue"]): + + if any(callable(_get_segment_for_color(cmap, c)) for c in ["red", "green", "blue"]): # type: ignore[arg-type] return False return True @@ -1000,15 +1037,25 @@ def combined_cmap( cmap2 = matplotlib.colormaps[cmap2] if all(_is_segment_iterable(c) for c in (cmap1, cmap2)): - segnew = {} + cmap1 = cast( + matplotlib.colors.LinearSegmentedColormap, cmap1 + ) # to appease mypy + cmap2 = cast( + matplotlib.colors.LinearSegmentedColormap, cmap2 + ) # to appease mypy + + segnew: dict[ + Literal["red", "green", "blue", "alpha"], Sequence[tuple[float, ...]] + ] = {} + for c in ["red", "green", "blue"]: seg1_c, seg2_c = ( - np.asarray(cmap1._segmentdata[c]), - np.asarray(cmap2._segmentdata[c]), + np.asarray(_get_segment_for_color(cmap1, c)), # type: ignore[arg-type] + np.asarray(_get_segment_for_color(cmap2, c)), # type: ignore[arg-type] ) seg1_c[:, 0] = seg1_c[:, 0] * 0.5 seg2_c[:, 0] = seg2_c[:, 0] * 0.5 + 0.5 - segnew[c] = np.r_[seg1_c, seg2_c] + segnew[c] = np.r_[seg1_c, seg2_c] # type: ignore[index] cmap = matplotlib.colors.LinearSegmentedColormap( name=name, segmentdata=segnew, N=N ) @@ -1029,11 +1076,11 @@ def combined_cmap( def gen_2d_colormap( ldat, cdat, - cmap: matplotlib.colors.Colormap | str = None, + cmap: matplotlib.colors.Colormap | str | None = None, *, lnorm: plt.Normalize | None = None, cnorm: plt.Normalize | None = None, - background: Any = None, + background: ColorType | None = None, N: int = 256, ): """Generate a 2D colormap image from lightness and color data. @@ -1081,9 +1128,9 @@ def gen_2d_colormap( cnorm = plt.Normalize() if background is None: - background: tuple[float, float, float] = (1, 1, 1, 1) + background_arr: tuple[float, float, float, float] = (1.0, 1.0, 1.0, 1.0) else: - background: tuple[float, float, float] = matplotlib.colors.to_rgba(background) + background_arr = matplotlib.colors.to_rgba(background) ldat_masked = np.ma.masked_invalid(ldat) cdat_masked = np.ma.masked_invalid(cdat) @@ -1097,20 +1144,21 @@ def gen_2d_colormap( img = cmap(c_vals) img *= l_vals - img += (1 - l_vals) * background + img += (1 - l_vals) * background_arr - l_linear = lnorm(np.linspace(lnorm.vmin, lnorm.vmax, N))[:, np.newaxis, np.newaxis] - cmap_img = np.repeat( - cmap(cnorm(np.linspace(cnorm.vmin, cnorm.vmax, N)))[np.newaxis, :], N, 0 - ) + lmin, lmax = cast(float, lnorm.vmin), cast(float, lnorm.vmax) # to appease mypy + cmin, cmax = cast(float, cnorm.vmin), cast(float, cnorm.vmax) + + l_linear = lnorm(np.linspace(lmin, lmax, N))[:, np.newaxis, np.newaxis] + cmap_img = np.repeat(cmap(cnorm(np.linspace(cmin, cmax, N)))[np.newaxis, :], N, 0) cmap_img *= l_linear - cmap_img += (1 - l_linear) * background + cmap_img += (1 - l_linear) * background_arr return cmap_img, img -def color_distance(c1, c2) -> float: - """Calculate the color distance between two RGB colors. +def color_distance(c1: ColorType, c2: ColorType) -> float: + """Calculate the color distance between two matplotlib colors. Parameters ---------- @@ -1143,7 +1191,7 @@ def color_distance(c1, c2) -> float: return np.sqrt((2 + r) * dR2 + 4 * dG2 + (2 + 255 / 256 - r) * dB2) -def close_to_white(c) -> bool: +def close_to_white(c: ColorType) -> bool: """Check if a given color is closer to white than black. Parameters @@ -1188,7 +1236,9 @@ def image_is_light( return close_to_white(prominent_color(im)) -def axes_textcolor(ax: matplotlib.axes.Axes, light="k", dark="w"): +def axes_textcolor( + ax: matplotlib.axes.Axes, light: ColorType = "k", dark: ColorType = "w" +): """Determine the text color based on the color of the mappable in an axes. Parameters diff --git a/src/erlab/plotting/general.py b/src/erlab/plotting/general.py index e621673b..d3cf85f1 100644 --- a/src/erlab/plotting/general.py +++ b/src/erlab/plotting/general.py @@ -17,10 +17,11 @@ import contextlib import copy -from typing import TYPE_CHECKING, Any, Literal +from collections.abc import Iterable +from typing import TYPE_CHECKING, Any, Literal, Union, cast import matplotlib -import matplotlib.colors as mcolors +import matplotlib.colors import matplotlib.image import matplotlib.patches import matplotlib.path @@ -40,7 +41,7 @@ ) if TYPE_CHECKING: - from collections.abc import Callable, Iterable, Sequence + from collections.abc import Callable, Collection, Sequence figure_width_ref = { "aps": [3.4, 7.0], @@ -119,20 +120,22 @@ def __init__( textOn: bool = True, useblit: bool = True, textprops: dict | None = None, - **lineprops: dict, + **lineprops, ): super().__init__(ax) if textprops is None: textprops = {} - self.connect_event("motion_notify_event", self.onmove) - self.connect_event("draw_event", self.clear) + self.connect_event("motion_notify_event", self.onmove) # type: ignore[arg-type] + self.connect_event("draw_event", self.clear) # type: ignore[arg-type] self.visible = True self.horizOn = horizOn self.vertOn = vertOn self.textOn = textOn + if self.canvas is None: + raise RuntimeError("No canvas found to attach to") self.useblit = useblit and self.canvas.supports_blit if self.useblit: @@ -291,14 +294,14 @@ def plot_array( colorbar: bool = False, colorbar_kw: dict | None = None, gamma: float = 1.0, - norm: mcolors.Normalize | None = None, + norm: matplotlib.colors.Normalize | None = None, xlim: float | tuple[float, float] | None = None, ylim: float | tuple[float, float] | None = None, crop: bool = False, rad2deg: bool | Iterable[str] = False, func: Callable | None = None, func_args: dict | None = None, - **improps: dict, + **improps, ) -> matplotlib.image.AxesImage: """Plots a 2D :class:`xarray.DataArray` using :func:`matplotlib.pyplot.imshow`. @@ -340,12 +343,16 @@ def plot_array( if isinstance(arr, np.ndarray): arr = xr.DataArray(arr) + if ax is None: ax = plt.gca() - if xlim is not None and not np.iterable(xlim): + + if xlim is not None and not isinstance(xlim, Iterable): xlim = (-xlim, xlim) - if ylim is not None and not np.iterable(ylim): + + if ylim is not None and not isinstance(ylim, Iterable): ylim = (-ylim, ylim) + if rad2deg is not False: if np.iterable(rad2deg): conv_dims = rad2deg @@ -368,7 +375,7 @@ def plot_array( colorbar_kw.setdefault("extend", "max") if norm is None: - norm = copy.deepcopy(mcolors.PowerNorm(gamma, **norm_kw)) + norm = copy.deepcopy(matplotlib.colors.PowerNorm(gamma, **norm_kw)) improps_default = { "interpolation": "none", @@ -385,19 +392,24 @@ def plot_array( arr = arr.copy(deep=True).sel({arr.dims[1]: slice(*xlim)}) if ylim is not None: arr = arr.copy(deep=True).sel({arr.dims[0]: slice(*ylim)}) + if func is not None: img = ax.imshow(func(arr.values, **func_args), norm=norm, **improps) else: img = ax.imshow(arr.values, norm=norm, **improps) - ax.set_xlabel(arr.dims[1]) - ax.set_ylabel(arr.dims[0]) + + ax.set_xlabel(str(arr.dims[1])) + ax.set_ylabel(str(arr.dims[0])) fancy_labels(ax) + if xlim is not None: ax.set_xlim(*xlim) if ylim is not None: ax.set_ylim(*ylim) + if colorbar: nice_colorbar(ax=ax, **colorbar_kw) + return img @@ -409,16 +421,16 @@ def plot_array_2d( normalize_with_larr: bool = False, xlim: float | tuple[float, float] | None = None, ylim: float | tuple[float, float] | None = None, - cmap: mcolors.Colormap | str = None, - lnorm: mcolors.Normalize | None = None, - cnorm: mcolors.Normalize | None = None, + cmap: matplotlib.colors.Colormap | str | None = None, + lnorm: matplotlib.colors.Normalize | None = None, + cnorm: matplotlib.colors.Normalize | None = None, background: Any = None, colorbar: bool = True, cax: matplotlib.axes.Axes | None = None, colorbar_kw: dict | None = None, imshow_kw: dict | None = None, N: int = 256, - **indexers_kwargs: dict, + **indexers_kwargs, ): if lnorm is None: lnorm = plt.Normalize() @@ -446,16 +458,19 @@ def plot_array_2d( larr = larr.qsel(**indexers_kwargs).copy(deep=True) carr = carr.qsel(**indexers_kwargs).copy(deep=True) sel_kw = {} + if xlim is not None: - if not np.iterable(xlim): + if not isinstance(xlim, Iterable): xlim = (-xlim, xlim) sel_kw[larr.dims[1]] = slice(*xlim) + if ylim is not None: - if not np.iterable(ylim): + if not isinstance(ylim, Iterable): ylim = (-ylim, ylim) sel_kw[larr.dims[0]] = slice(*ylim) - larr = larr.sel(**sel_kw) - carr = carr.sel(**sel_kw) + + larr = larr.sel(sel_kw) + carr = carr.sel(sel_kw) if normalize_with_larr: carr = carr / larr @@ -478,23 +493,34 @@ def plot_array_2d( if colorbar: if cax is None: + fig = ax.get_figure() + if fig is None: + raise ValueError( + "Cannot create colorbar without a figure. Please provide `cax`." + ) + colorbar_kw.setdefault("aspect", 2) colorbar_kw.setdefault("anchor", (0, 1)) colorbar_kw.setdefault("panchor", (0, 1)) - cb = ax.get_figure().colorbar(plt.cm.ScalarMappable(), ax=ax, **colorbar_kw) + + cb = fig.colorbar(plt.cm.ScalarMappable(), ax=ax, **colorbar_kw) cax = cb.ax cax.clear() + lmin, lmax = cast(float, lnorm.vmin), cast(float, lnorm.vmax) # to appease mypy + cmin, cmax = cast(float, cnorm.vmin), cast(float, cnorm.vmax) + cax.imshow( cmap_img.transpose(1, 0, 2), - extent=(lnorm.vmin, lnorm.vmax, cnorm.vmin, cnorm.vmax), + extent=(lmin, lmax, cmin, cmax), origin="lower", aspect="auto", ) im = ax.imshow(img, extent=array_extent(larr), **imshow_kw) - ax.set_xlabel(larr.dims[0]) - ax.set_ylabel(larr.dims[1]) + ax.set_xlabel(str(larr.dims[0])) + ax.set_ylabel(str(larr.dims[1])) + fancy_labels(ax) if colorbar: return im, cb @@ -503,11 +529,11 @@ def plot_array_2d( def gradient_fill( - x: Sequence[int | float], - y: Sequence[int | float], + x: Collection[int | float], + y: Collection[int | float], y0: float | None = None, color: str | tuple[float, float, float] | tuple[float, float, float, float] = "C0", - cmap: str | mcolors.Colormap | None = None, + cmap: str | matplotlib.colors.Colormap | None = None, transpose: bool = False, reverse: bool = False, ax: matplotlib.axes.Axes | None = None, @@ -543,8 +569,8 @@ def gradient_fill( kwargs.setdefault("norm", InversePowerNorm(0.5)) kwargs.setdefault("alpha", 0.75) if cmap is None: - cmap = mcolors.LinearSegmentedColormap.from_list( - "", colors=[(1, 1, 1, 0), mcolors.to_rgba(color)], N=1024 + cmap = matplotlib.colors.LinearSegmentedColormap.from_list( + "", colors=[(1, 1, 1, 0), matplotlib.colors.to_rgba(color)], N=1024 ) if isinstance(cmap, str): cmap = matplotlib.colormaps[cmap] @@ -559,6 +585,8 @@ def gradient_fill( if y0 is None: y0 = min(y) + + x = np.asarray(x) xn = np.r_[x[0], x, x[-1]] yn = np.r_[y0, y, y0] patch = matplotlib.patches.PathPatch( @@ -569,7 +597,7 @@ def gradient_fill( im = matplotlib.image.AxesImage( ax, cmap=cmap, interpolation="bicubic", origin="lower", zorder=0, **kwargs ) - im.use_sticky_edges = False + im.use_sticky_edges = False # type: ignore[attr-defined] ax.add_artist(im) if transpose: im.set_data(np.linspace(0, 1, 1024).reshape(1024, 1).T) @@ -598,8 +626,15 @@ def plot_slices( colorbar: Literal["none", "right", "rightspan", "all"] = "none", hide_colorbar_ticks: bool = True, annotate: bool = True, - cmap: str | mcolors.Colormap | Iterable[mcolors.Colormap | str] | None = None, - norm: mcolors.Normalize | Iterable[mcolors.Normalize] | None = None, + cmap: str + | matplotlib.colors.Colormap + | Iterable[ + str | matplotlib.colors.Colormap | Iterable[matplotlib.colors.Colormap | str] + ] + | None = None, + norm: matplotlib.colors.Normalize + | Iterable[matplotlib.colors.Normalize | Iterable[matplotlib.colors.Normalize]] + | None = None, order: Literal["C", "F"] = "C", cmap_order: Literal["C", "F"] = "C", norm_order: Literal["C", "F"] | None = None, @@ -608,9 +643,9 @@ def plot_slices( subplot_kw: dict | None = None, annotate_kw: dict | None = None, colorbar_kw: dict | None = None, - axes: npt.NDArray[matplotlib.axes.Axes] | None = None, - **values: dict, -) -> tuple[matplotlib.figure.Figure, npt.NDArray[matplotlib.axes.Axes]]: + axes: Iterable[matplotlib.axes.Axes] | None = None, + **values, +) -> tuple[matplotlib.figure.Figure, Iterable[matplotlib.axes.Axes]]: """Automated comparison plot of slices. Parameters @@ -766,18 +801,18 @@ def plot_slices( slice_levels = slice_kw[slice_dim] slice_width = kwargs.pop(slice_dim + "_width", None) - plot_dims = [d for d in dims if d != slice_dim] + plot_dims: list[str] = [str(d) for d in dims if d != slice_dim] if len(plot_dims) not in (1, 2): raise ValueError("The data to plot must be 1D or 2D") - if not np.iterable(slice_levels): + if not isinstance(slice_levels, Iterable): slice_levels = [slice_levels] - if xlim is not None and not np.iterable(xlim): + if xlim is not None and not isinstance(xlim, Iterable): xlim = (-xlim, xlim) - if ylim is not None and not np.iterable(ylim): + if ylim is not None and not isinstance(ylim, Iterable): ylim = (-ylim, ylim) auto_gradient_color = all(k not in gradient_kw for k in ("c", "color")) @@ -796,10 +831,16 @@ def plot_slices( cmap_name = cmap cmap_norm = norm + if axes is None: fig, axes = plt.subplots(nrow, ncol, figsize=figsize, **subplot_kw) - + axes = cast(npt.NDArray[Any], axes) else: + if not isinstance(axes, np.ndarray): + if not isinstance(axes, Iterable): + raise TypeError("axes must be an iterable of matplotlib.axes.Axes") + axes = np.array(axes, dtype=object) + fig = axes.flat[0].get_figure() if nrow == 1: @@ -808,7 +849,7 @@ def plot_slices( if ncol == 1: axes = axes[:, np.newaxis].reshape(-1, 1) - qsel_kw = {} + qsel_kw: dict[str, Any] = {} if crop: if len(plot_dims) == 1: @@ -825,7 +866,7 @@ def plot_slices( if ylim is not None: qsel_kw[plot_dims[0]] = slice(*ylim) - if slice_width is not None: + if slice_width is not None and slice_dim is not None: qsel_kw[slice_dim + "_width"] = slice_width for i in range(len(slice_levels)): @@ -840,17 +881,26 @@ def plot_slices( elif order == "C": ax = axes[j, i] - if np.iterable(cmap_name) and not isinstance(cmap_name, str): + if isinstance(cmap_name, Iterable) and not isinstance(cmap_name, str): + cmap_name = list(cmap_name) if cmap_order == "F": - if isinstance(cmap_name[i], str): + if isinstance(cmap_name[i], str | matplotlib.colors.Colormap): cmap = cmap_name[i] else: - cmap = cmap_name[i][j] + cmap = list( + cast( + Iterable[str | matplotlib.colors.Colormap], cmap_name[i] + ) + )[j] elif cmap_order == "C": - if isinstance(cmap_name[j], str): + if isinstance(cmap_name[j], str | matplotlib.colors.Colormap): cmap = cmap_name[j] else: - cmap = cmap_name[j][i] + cmap = list( + cast( + Iterable[str | matplotlib.colors.Colormap], cmap_name[j] + ) + )[i] else: cmap = cmap_name @@ -884,21 +934,24 @@ def plot_slices( ) elif len(plot_dims) == 2: - if np.iterable(cmap_norm): + if isinstance(cmap_norm, Iterable): + cmap_norm = list(cmap_norm) if norm_order == "F": try: - norm = cmap_norm[i][j] + norm = list(cast(Iterable[plt.Normalize], cmap_norm[i]))[j] except TypeError: norm = cmap_norm[i] elif norm_order == "C": try: - norm = cmap_norm[j][i] + norm = list(cast(Iterable[plt.Normalize], cmap_norm[j]))[i] except TypeError: norm = cmap_norm[j] else: norm = copy.deepcopy(cmap_norm) - plot_array(dat_sel, ax=ax, norm=norm, cmap=cmap, **kwargs) + plot_array( + dat_sel, ax=ax, norm=cast(plt.Normalize, norm), cmap=cmap, **kwargs + ) if same_limits and len(plot_dims) == 2: vmn, vmx = [], [] @@ -939,12 +992,15 @@ def plot_slices( return fig, axes +MultipleLine2D = list[Union[matplotlib.lines.Line2D, "MultipleLine2D"]] + + def fermiline( ax: matplotlib.axes.Axes | None = None, value: float = 0.0, orientation: Literal["h", "v"] = "h", **kwargs, -) -> matplotlib.lines.Line2D: +) -> matplotlib.lines.Line2D | MultipleLine2D: """Plots a constant energy line to denote the Fermi level. Parameters diff --git a/src/erlab/plotting/plot3d.py b/src/erlab/plotting/plot3d.py index 8e916fa5..69fda946 100644 --- a/src/erlab/plotting/plot3d.py +++ b/src/erlab/plotting/plot3d.py @@ -63,7 +63,7 @@ def set_3d_properties(self, verts, zs=0, zdir="z"): self._segment3d = np.asarray( [ (*np.dot(_transform_zdir(zdir), (x, y, 0)), 0, 0, z) - for ((x, y), z) in zip(verts, zs) + for ((x, y), z) in zip(verts, zs, strict=True) ] ) diff --git a/tests/accessors/test_fit.py b/tests/accessors/test_fit.py index cbe6c83f..3eb60b5b 100644 --- a/tests/accessors/test_fit.py +++ b/tests/accessors/test_fit.py @@ -130,7 +130,9 @@ def sine(t, a, f, p): # params as DataArray of JSON strings params = [] - for a, p, f in zip(a_guess, p_guess, np.full_like(da.x, 2, dtype=float)): + for a, p, f in zip( + a_guess, p_guess, np.full_like(da.x, 2, dtype=float), strict=True + ): params.append(lmfit.create_params(a=a, p=p, f=f).dumps()) params = xr.DataArray(params, coords=[da.x]) fit = da.modelfit( diff --git a/tests/analysis/test_fit_functions_dynamic.py b/tests/analysis/test_fit_functions_dynamic.py index 30966595..ba53abcd 100644 --- a/tests/analysis/test_fit_functions_dynamic.py +++ b/tests/analysis/test_fit_functions_dynamic.py @@ -33,7 +33,7 @@ def test_poly_func_call(): x = np.arange(5, dtype=np.float64) coeffs = RAND_STATE.randn(3) expected_result = np.polyval(np.asarray(list(reversed(coeffs))), x) - params = dict(zip([f"c{i}" for i in range(3)], coeffs)) + params = dict(zip([f"c{i}" for i in range(3)], coeffs, strict=True)) result = PolynomialFunction(degree=2)(x, **params) assert np.allclose(result, expected_result) diff --git a/tests/analysis/test_kspace.py b/tests/analysis/test_kspace.py index 7ec8062e..a450807e 100644 --- a/tests/analysis/test_kspace.py +++ b/tests/analysis/test_kspace.py @@ -18,6 +18,7 @@ def _generate_funclist() -> list[tuple[Callable, Callable]]: [0, 30.0, -30.0], [0.0, 10.0, -10.0], [0.0, 10.0, -10.0], + strict=True, ): funcs.append(kconv_func(k_tot, delta, xi, xi0, beta0)) for kconv_func in ( @@ -30,6 +31,7 @@ def _generate_funclist() -> list[tuple[Callable, Callable]]: [0.0, 10.0, -10.0], [0.0, 10.0, -10.0], [0.0, 10.0, -10.0], + strict=True, ): funcs.append(kconv_func(k_tot, delta, chi, chi0, xi, xi0)) return funcs diff --git a/tests/io/test_dataloader.py b/tests/io/test_dataloader.py index 2c8d9658..7b438f4c 100644 --- a/tests/io/test_dataloader.py +++ b/tests/io/test_dataloader.py @@ -4,12 +4,13 @@ import os import re import tempfile +from typing import ClassVar import erlab.io import numpy as np import pandas as pd -from erlab.io.exampledata import generate_data_angles from erlab.io.dataloader import LoaderBase +from erlab.io.exampledata import generate_data_angles def make_data(beta=5.0, temp=20.0, hv=50.0, bandshift=0.0): @@ -88,9 +89,9 @@ def test_loader(): class ExampleLoader(LoaderBase): name = "example" - aliases = ["Ex"] + aliases = ("Ex",) - name_map = { + name_map: ClassVar[dict] = { "eV": "BindingEnergy", "alpha": "ThetaX", "beta": [ @@ -107,7 +108,7 @@ class ExampleLoader(LoaderBase): "temp_sample": "TB", } - coordinate_attrs: tuple[str, ...] = ( + coordinate_attrs = ( "beta", "delta", "xi", @@ -121,7 +122,7 @@ class ExampleLoader(LoaderBase): # Attributes to be used as coordinates. Place all attributes that we don't want to # lose when merging multiple file scans here. - additional_attrs = { + additional_attrs: ClassVar[dict] = { "configuration": 1, # Experimental geometry. Required for momentum conversion "sample_workfunction": 4.3, } # Any additional metadata you want to add to the data