diff --git a/doc/user_guide/eels.rst b/doc/user_guide/eels.rst index fd1066e71..ddfbfcb2e 100644 --- a/doc/user_guide/eels.rst +++ b/doc/user_guide/eels.rst @@ -274,6 +274,12 @@ The model for the GOS can be specified with the ``GOS`` argument By default, a freely usable tabulated dataset, in `gosh `__ format, is downloaded from Zenodo: `doi:10.5281/zenodo.7645765 `_. +As an alternative, one can use the `Dirac GOS `_ to include the relativistic effects using the Dirac solution, which can be downloaded from Zenodo: `doi:10.5281/zenodo.12800856 `_. The Dirac GOS can be used as follows: + +.. code-block:: python + + >>> m = s.create_model(ll=ll, GOS="Dirac") + Custom GOS saved in the `gosh `__ format can be used, the following example download a previous version (1.0) of the GOS file from Zenodo diff --git a/examples/model_fitting/EELS_curve_fitting.py b/examples/model_fitting/EELS_curve_fitting.py index 4087127c8..783fe9db3 100644 --- a/examples/model_fitting/EELS_curve_fitting.py +++ b/examples/model_fitting/EELS_curve_fitting.py @@ -1,3 +1,4 @@ +# %% """ EELS curve fitting ================== @@ -21,3 +22,11 @@ m.enable_fine_structure() m.multifit(kind="smart") m.plot() + +# one can also use the Dirac GOS by specifying the GOS parameter +m = s.create_model(low_loss=ll, GOS="dirac") +m.enable_fine_structure() +m.multifit(kind="smart") +m.plot() + +# %% diff --git a/exspy/components/eels_cl_edge.py b/exspy/components/eels_cl_edge.py index 3a2f9d8dd..56f4eb965 100644 --- a/exspy/components/eels_cl_edge.py +++ b/exspy/components/eels_cl_edge.py @@ -19,18 +19,19 @@ import functools import logging +import warnings import math import numpy as np from scipy.interpolate import splev from hyperspy.component import Component -from exspy.misc.eels.gosh_gos import GoshGOS, _GOSH_DOI +from exspy.misc.eels.gosh_gos import GoshGOS, GOSH_SOURCES from exspy.misc.eels.hartree_slater_gos import HartreeSlaterGOS from exspy.misc.eels.hydrogenic_gos import HydrogenicGOS from exspy.misc.eels.effective_angle import effective_angle from hyperspy.ui_registry import add_gui_method - +from hyperspy.exceptions import VisibleDeprecationWarning _logger = logging.getLogger(__name__) @@ -75,8 +76,9 @@ class EELSCLEdge(Component): The preferred option is to use a database of cross sections in GOSH format. One such database can be freely downloaded from Zenodo at: - https://doi.org/%s while information on the GOSH format - are available at: https://gitlab.com/gguzzina/gosh . + https://doi.org/%s while information on the GOSH format are available at: https://gitlab.com/gguzzina/gosh. Alternatively, + one can use the Dirac GOSH database to include relativistic effects, + available at: https://doi.org/%s. eXSpy also supports Peter Rez's Hartree Slater cross sections parametrised as distributed by Gatan in their Digital Micrograph (DM) @@ -92,11 +94,11 @@ class EELSCLEdge(Component): Usually a string, for example, ``'Ti_L3'`` for the GOS of the titanium L3 subshell. If a dictionary is passed, it is assumed that Hartree Slater GOS was exported using `GOS.as_dictionary`, and will be reconstructed. - GOS : ``'gosh'``, ``'hydrogenic'``, ``'Hartree-Slater'`` or str - The GOS to use. Default is ``'gosh'``. If str, it must the path to gosh - GOS file. + GOS : ``'dft'``,``'dirac'``, ``'hydrogenic'``, ``'Hartree-Slater'`` or str + The GOS to use. Default is ``'dft'``. If str, it must the path to gosh GOS file. + The ``'dft'`` and ``'dirac'`` databases are in the ``'gosh'`` format. gos_file_path : str, None - Only with ``GOS='gosh'``. Specify the file path of the gosh file + Only with ``GOS='dft' or 'dirac'``. Specify the file path of the gosh file to use. If None, use the file from https://doi.org/%s Attributes @@ -146,7 +148,7 @@ class EELSCLEdge(Component): _fine_structure_coeff_free = True _fine_structure_spline_active = True - def __init__(self, element_subshell, GOS="gosh", gos_file_path=None): + def __init__(self, element_subshell, GOS="dft", gos_file_path=None): # Declare the parameters self.fine_structure_components = FSet(component=self) Component.__init__( @@ -168,14 +170,28 @@ def __init__(self, element_subshell, GOS="gosh", gos_file_path=None): self.GOS = None if GOS == "gosh": - self.GOS = GoshGOS(element_subshell, gos_file_path=gos_file_path) + warnings.warn( + "The value 'gosh' of the `GOS` parameter has been renamed to 'dft' in " + "eXSpy 0.3.0, use `GOS='dft'` instead. " + "Using `GOS='gosh'` will stop working in eXSpy 1.0.", + VisibleDeprecationWarning, + ) + GOS = "dft" + if GOS == "dft": + self.GOS = GoshGOS( + element_subshell, gos_file_path=gos_file_path, source="dft" + ) + elif GOS == "dirac": + self.GOS = GoshGOS( + element_subshell, gos_file_path=gos_file_path, source="dirac" + ) elif GOS == "Hartree-Slater": # pragma: no cover self.GOS = HartreeSlaterGOS(element_subshell) elif GOS == "hydrogenic": self.GOS = HydrogenicGOS(element_subshell) else: raise ValueError( - "GOS must be one of 'gosh', 'hydrogenic' or 'Hartree-Slater'." + "GOS must be one of 'dft', 'dirac','hydrogenic' or 'Hartree-Slater'." ) self.onset_energy.value = self.GOS.onset_energy self.onset_energy.free = False @@ -187,7 +203,9 @@ def __init__(self, element_subshell, GOS="gosh", gos_file_path=None): self.intensity.bmax = None self._whitelist["GOS"] = ("init", GOS) - if GOS == "gosh": + if GOS == "dft": + self._whitelist["element_subshell"] = ("init", self.GOS.as_dictionary(True)) + elif GOS == "dirac": self._whitelist["element_subshell"] = ("init", self.GOS.as_dictionary(True)) elif GOS == "Hartree-Slater": # pragma: no cover self._whitelist["element_subshell"] = ("init", self.GOS.as_dictionary(True)) @@ -535,4 +553,8 @@ def as_dictionary(self, fullcopy=True): return dic -EELSCLEdge.__doc__ %= (_GOSH_DOI, _GOSH_DOI) +EELSCLEdge.__doc__ %= ( + GOSH_SOURCES["dft"]["DOI"], + GOSH_SOURCES["dirac"]["DOI"], + GOSH_SOURCES["dft"]["DOI"], +) diff --git a/exspy/docstrings/model.py b/exspy/docstrings/model.py index 30db3d820..0d76d0de7 100644 --- a/exspy/docstrings/model.py +++ b/exspy/docstrings/model.py @@ -19,14 +19,16 @@ """Common docstring snippets for model.""" -from exspy.misc.eels.gosh_gos import _GOSH_DOI +from exspy.misc.eels.gosh_gos import GOSH_SOURCES -GOS_PARAMETER = """GOS : 'hydrogenic', 'gosh', 'Hartree-Slater'. - The GOS to use. Default is ``'gosh'``. +GOS_PARAMETER = """GOS : 'hydrogenic', 'dft', 'dirac', 'Hartree-Slater'. + The GOS to use. Default is ``'dft'``. gos_file_path : str, None - Only with GOS='gosh'. Specify the file path of the gosh file - to use. If None, use the file from doi:{}""".format(_GOSH_DOI) + Only with GOS='dft' or 'dirac'. Specify the file path of the gosh file + to use. If None, use the file from doi:{}""".format( + GOSH_SOURCES["dft"]["DOI"] +) EELSMODEL_PARAMETERS = """ll : None or EELSSpectrum If an EELSSpectrum is provided, it will be assumed that it is diff --git a/exspy/misc/eels/gosh_gos.py b/exspy/misc/eels/gosh_gos.py index 74f355df6..e3e710ce0 100644 --- a/exspy/misc/eels/gosh_gos.py +++ b/exspy/misc/eels/gosh_gos.py @@ -32,9 +32,17 @@ R = constants.value("Rydberg constant times hc in eV") a0 = constants.value("Bohr radius") -_GOSH_DOI = "10.5281/zenodo.7645765" -_GOSH_URL = f"doi:{_GOSH_DOI}/Segger_Guzzinati_Kohl_1.5.0.gosh" -_GOSH_KNOWN_HASH = "md5:7fee8891c147a4f769668403b54c529b" +DFT_GOSH = { + "DOI": "10.5281/zenodo.7645765", + "URL": "doi:10.5281/zenodo.7645765/Segger_Guzzinati_Kohl_1.5.0.gosh", + "KNOWN_HASH": "md5:7fee8891c147a4f769668403b54c529b", +} +DIRAC_GOSH = { + "DOI": "10.5281/zenodo.12800856", + "URL": "doi:10.5281/zenodo.12800856/Dirac_GOS_compact.gosh", + "KNOWN_HASH": "md5:01a855d3750d2c063955248358dbee8d", +} +GOSH_SOURCES = {"dft": DFT_GOSH, "dirac": DIRAC_GOSH} class GoshGOS(TabulatedGOS): @@ -79,7 +87,7 @@ class GoshGOS(TabulatedGOS): "subshell_factor": None, } - def __init__(self, element_subshell, gos_file_path=None): + def __init__(self, element_subshell, gos_file_path=None, source="dft"): """ Parameters ---------- @@ -87,12 +95,17 @@ def __init__(self, element_subshell, gos_file_path=None): For example, 'Ti_L3' for the GOS of the titanium L3 subshell gos_file_path : str The path of the gosh file to use. + source : str + The source of the GOS data. Options are 'dft' or 'dirac'. """ if gos_file_path is None: + source = source.lower() + assert source in GOSH_SOURCES.keys(), f"Invalid source: {source}" + self._name = source gos_file_path = pooch.retrieve( - url=_GOSH_URL, - known_hash=_GOSH_KNOWN_HASH, + url=GOSH_SOURCES[source]["URL"], + known_hash=GOSH_SOURCES[source]["KNOWN_HASH"], progressbar=preferences.General.show_progressbar, ) self.gos_file_path = gos_file_path diff --git a/exspy/models/eelsmodel.py b/exspy/models/eelsmodel.py index b6b6a8d8d..accbcef6f 100644 --- a/exspy/models/eelsmodel.py +++ b/exspy/models/eelsmodel.py @@ -67,14 +67,16 @@ def __init__( auto_background=True, auto_add_edges=True, low_loss=None, - GOS="gosh", + GOS="dft", dictionary=None, + gos_file_path=None, ): """ Build an EELS model. Parameters ---------- + GOS : Generalized Oscillator Strength, availiable option in ['hydrogenic', 'dft', 'dirac', 'Hartree-Slater'], default is 'dft'. spectrum : a EELSSpectrum instance %s @@ -94,6 +96,7 @@ def __init__( self.convolution_axis = None self.low_loss = low_loss self.GOS = GOS + self.gos_file_path = gos_file_path self.edges = [] self._background_components = [] self._whitelist.update( @@ -410,10 +413,9 @@ def _add_edges_from_subshells_names(self, e_shells=None): if e_shells is None: e_shells = list(self.signal.subshells) e_shells.sort() - master_edge = EELSCLEdge(e_shells.pop(), self.GOS) - # If self.GOS was None, the GOS is set by eels_cl_edge so - # we reassing the value of self.GOS - self.GOS = master_edge.GOS._name + master_edge = EELSCLEdge( + e_shells.pop(), self.GOS, gos_file_path=self.gos_file_path + ) self.append(master_edge) element = master_edge.element while len(e_shells) > 0: @@ -429,7 +431,9 @@ def _add_edges_from_subshells_names(self, e_shells=None): # Add the other subshells of the same element # and couple their intensity and onset_energy to that of the # master edge - edge = EELSCLEdge(e_shells.pop(), GOS=self.GOS) + edge = EELSCLEdge( + e_shells.pop(), GOS=self.GOS, gos_file_path=self.gos_file_path + ) edge.intensity.twin = master_edge.intensity edge.onset_energy.twin = master_edge.onset_energy diff --git a/exspy/signals/eels.py b/exspy/signals/eels.py index 4dbf0bcc2..c30524307 100644 --- a/exspy/signals/eels.py +++ b/exspy/signals/eels.py @@ -1626,7 +1626,7 @@ def create_model( low_loss=None, auto_background=True, auto_add_edges=True, - GOS="gosh", + GOS="dft", gos_file_path=None, dictionary=None, ): @@ -1635,6 +1635,7 @@ def create_model( Parameters ---------- %s + GOS: Generalized Oscillator Strength, availiable option in ['hydrogenic', 'dft', 'dirac', 'Hartree-Slater'], default is 'dft' Returns ------- @@ -1660,6 +1661,7 @@ def create_model( auto_add_edges=auto_add_edges, GOS=GOS, dictionary=dictionary, + gos_file_path=gos_file_path, ) return model diff --git a/exspy/tests/components/test_eels_cl_edge.py b/exspy/tests/components/test_eels_cl_edge.py index 88eb9e111..2d25e2de3 100644 --- a/exspy/tests/components/test_eels_cl_edge.py +++ b/exspy/tests/components/test_eels_cl_edge.py @@ -49,3 +49,29 @@ def test_restore_EELS_model(tmp_path): s2 = hs.load(fname) m3 = s2.models.restore(model_name) np.testing.assert_allclose(m.as_signal(), m3.as_signal()) + + +def test_restore_EELS_model_dirac(tmp_path): + s = hs.load(TEST_DATA_DIR / "coreloss_spectrum.msa", signal_type="EELS") + ll = hs.load(TEST_DATA_DIR / "lowloss_spectrum.msa", signal_type="EELS") + + s.add_elements(("Mn", "O")) + s.set_microscope_parameters( + beam_energy=300, convergence_angle=24.6, collection_angle=13.6 + ) + + m = s.create_model(low_loss=ll, GOS="dirac") + m.enable_fine_structure() + m.multifit(kind="smart") + + model_name = "fit1" + m.store(model_name) + fname = tmp_path / "test_save_eelsmodel.hspy" + s.save(tmp_path / fname) + m2 = s.models.restore(model_name) + + np.testing.assert_allclose(m.as_signal(), m2.as_signal()) + + s2 = hs.load(fname) + m3 = s2.models.restore(model_name) + np.testing.assert_allclose(m.as_signal(), m3.as_signal()) diff --git a/exspy/tests/misc/test_gos.py b/exspy/tests/misc/test_gos.py index dccb8bc0c..eaac1e787 100644 --- a/exspy/tests/misc/test_gos.py +++ b/exspy/tests/misc/test_gos.py @@ -80,3 +80,16 @@ def test_binding_energy_database(): # These elements are not in the database if element not in ["Bk", "Cf", "Cm", "metadata"]: assert "Binding_energies" in elements[element]["Atomic_properties"].keys() + + +def test_dirac_gosh_not_in_conventions(): + gos = GoshGOS("Ti_L2", source="dirac") + gos.subshell = "L234" + with pytest.raises(ValueError): + gos.read_gos_data() + + +def test_dirac_gosh_not_in_file(): + # Dirac GOS which doesn't have the Uue element + with pytest.raises(ValueError): + _ = GoshGOS("Uue_L3", source="dirac") diff --git a/exspy/tests/models/test_eelsmodel.py b/exspy/tests/models/test_eelsmodel.py index e2fcd1a1e..1e8155478 100644 --- a/exspy/tests/models/test_eelsmodel.py +++ b/exspy/tests/models/test_eelsmodel.py @@ -27,8 +27,9 @@ import hyperspy.api as hs from exspy.misc.elements import elements_db as elements from hyperspy.decorators import lazifyTestClass -from exspy.misc.eels.gosh_gos import _GOSH_URL, _GOSH_KNOWN_HASH +from exspy.misc.eels.gosh_gos import DFT_GOSH, DIRAC_GOSH from exspy.signals import EELSSpectrum +from hyperspy.exceptions import VisibleDeprecationWarning # Dask does not always work nicely with np.errstate, @@ -72,8 +73,25 @@ def test_gos_hydrogenic(self): m.fit() def test_gos_gosh(self): - m = self.s.create_model(auto_add_edges=True, GOS="gosh") - assert m["B_K"].GOS._name == "gosh" + with pytest.warns(VisibleDeprecationWarning): + m = self.s.create_model(auto_add_edges=True, GOS="gosh") + assert m["B_K"].GOS._name == "dft" + m.fit() + + with pytest.raises(ValueError): + self.s.create_model(auto_add_edges=True, GOS="not_a_GOS") + + def test_gos_gosh_dft(self): + m = self.s.create_model(auto_add_edges=True, GOS="dft") + assert m["B_K"].GOS._name == "dft" + m.fit() + + with pytest.raises(ValueError): + self.s.create_model(auto_add_edges=True, GOS="not_a_GOS") + + def test_gos_gosh_dirac(self): + m = self.s.create_model(auto_add_edges=True, GOS="dirac") + assert m["B_K"].GOS._name == "dirac" m.fit() with pytest.raises(ValueError): @@ -81,11 +99,20 @@ def test_gos_gosh(self): def test_gos_file(self): gos_file_path = pooch.retrieve( - url=_GOSH_URL, - known_hash=_GOSH_KNOWN_HASH, + url=DFT_GOSH["URL"], + known_hash=DFT_GOSH["KNOWN_HASH"], ) self.s.create_model(auto_add_edges=True, gos_file_path=gos_file_path) + def test_gos_file_dirac(self): + gos_file_path = pooch.retrieve( + url=DIRAC_GOSH["URL"], + known_hash=DIRAC_GOSH["KNOWN_HASH"], + ) + self.s.create_model( + auto_add_edges=True, gos_file_path=gos_file_path, GOS="dirac" + ) + def test_auto_add_background_true(self): m = self.s.create_model(auto_background=True) from hyperspy.components1d import PowerLaw diff --git a/upcoming_changes/72.new.rst b/upcoming_changes/72.new.rst new file mode 100644 index 000000000..735cb482b --- /dev/null +++ b/upcoming_changes/72.new.rst @@ -0,0 +1 @@ +Support for `Dirac GOS `_ in the gosh format for EELS quantification, which includes the relativistic effects (e.g. spin-orbit coupling) based on Dirac equation. \ No newline at end of file