Highly Charged Ion (HCI) Lines from NIST ASD

Motivation

We often find ourselves hard coding line center positions into mass, which is prone to errors and can be tedious when there are many lines of interest to insert. In addition, the line positions would need to be manually updated for any changes in established results. In the case of highly charged ions, such as those produced in an electron beam ion trap (EBIT), there is a vast number of potential lines coming from almost any charge state of almost any element. Luckily, these lines are well documented through the NIST Atomic Spectral Database (ASD). Here, we have parsed a NIST ASD SQL dump and converted it into an easily Python readable pickle file. The hci_lines.py module implements the NIST_ASD class, which loads that pickle file and contains useful functions for working with the ASD data. It also automatically adds in some of the more common HCI lines that we commonly use in our EBIT data analyses.

Exploring the methods of class NIST_ASD

The class NIST_ASD can be initialized without arguments if the user wants to use the default ASD pickle file. This file is located at mass/calibration/nist_asd.pickle. A custom pickle file can be used by passing in the pickleFilename argument during initialization. The methods of the NIST_ASD class are described below:

Usage examples

Next, we will demonstrate usage of these methods with the example of Ne, a commonly injected gas at the NIST EBIT.

# mkdocs: render
import mass2
import mass2.calibration.hci_lines
import numpy as numpy
import pylab as plt

test_asd = mass2.calibration.hci_lines.NIST_ASD()
availableElements = test_asd.getAvailableElements()
assert 'Ne' in availableElements
availableNeCharges = test_asd.getAvailableSpectralCharges(element='Ne')
assert 10 in availableNeCharges
subsetNe10Levels = test_asd.getAvailableLevels(element='Ne', spectralCharge=10, maxLevels=6, getUncertainty=False)
assert '2p 2P* J=1/2' in list(subsetNe10Levels.keys())
exampleNeLevel = test_asd.getSingleLevel(element='Ne', spectralCharge=10, conf='2p', term='2P*', JVal='1/2', getUncertainty=False)

print(availableElements[:10])
print(availableNeCharges)
for k, v in subsetNe10Levels.items():
  subsetNe10Levels[k] = round(v, 1)
print(subsetNe10Levels)
print(f'{exampleNeLevel:.1f}')
[np.str_('Sn'), np.str_('Cu'), np.str_('Na'), np.str_('As'), np.str_('Zn'), np.str_('Ne'), np.str_('Ge'), np.str_('Ga'), np.str_('Rb'), np.str_('Se')]
[9, 1, 2, 3, 4, 5, 6, 7, 8, 10]
{'1s 2S J=1/2': 0.0, '2p 2P* J=1/2': 1021.5, '2s 2S J=1/2': 1021.5, '2p 2P* J=3/2': 1022.0, '3p 2P* J=1/2': 1210.8, '3s 2S J=1/2': 1210.8}
1021.5

Functions for generating SpectralLine objects from ASD data

The module also contains some functions outside of the NIST_ASD class that are useful for integration with MASS. First, the add_hci_line function which, takes arguments that are relevant in HCI work, including as element, spectr_ch, energies, widths, and ratios. The function calls mass2.calibration.fluorescence_lines.addline, generates a line name with the given parameters, and populates the various fields. As an example, let us create a H-like Be line. Here, we assume a lorentzian width of 0.1 eV.

# mkdocs: render
test_element = 'Be'
test_charge = 4
test_conf = '2p'
test_term = '2P*'
test_JVal = '3/2'
test_level = f'{test_conf} {test_term} J={test_JVal}'
test_energy = test_asd.getSingleLevel(
    element=test_element, spectralCharge=test_charge,
    conf=test_conf, term=test_term, JVal=test_JVal, getUncertainty=False)
test_line = mass2.calibration.hci_lines.add_hci_line(element=test_element, spectr_ch=test_charge,
line_identifier=test_level, energies=[test_energy], widths=[0.1], ratios=[1.0])
assert test_line.peak_energy == test_energy

print(mass2.spectra[f'{test_element}{test_charge} {test_conf} {test_term} J={test_JVal}'])
print(f'{test_line.peak_energy:.1f}')
SpectralLine: Be4 2p 2P* J=3/2
163.3

The name format for grabbing the line from mass2.spectra is shown above. The transition is uniquely specified by the element, charge, configuration, term, and J value. Below, we show what this line looks like assuming a zero-width Gaussian component.

# mkdocs: render
test_line.plot()

The module contains two other functions which are used to easily generate some lines from levels that are commonly observed at the NIST EBIT. These functions are add_H_like_lines_from_asd and add_He_like_lines_from_asd. As the names imply, these functions add H- and He-like lines to mass using the data in the ASD pickle. These functions require the asd and element arguments and also contain the optional maxLevels argument, which works similarly as the argument in the class methods. The module also automatically adds H- and He-like lines for the most commonly used elements, which includes 'N', 'O', 'Ne', and 'Ar'. Below, we check that common elements are being added as spectralLine objects and then add some of the lower order H- and He-like Ga lines.

# mkdocs: render
print([mass2.spectra['O7 1s.2p 1P* J=1'], round(mass2.spectra['O7 1s.2p 1P* J=1'].peak_energy,1)])

test_element = 'Ga'
HLikeGaLines = mass2.calibration.hci_lines.add_H_like_lines_from_asd(asd=test_asd, element=test_element, maxLevels=6)
HeLikeGaLines = mass2.calibration.hci_lines.add_He_like_lines_from_asd(asd=test_asd, element=test_element, maxLevels=7)

print([[iLine, round(iLine.peak_energy, 1)] for iLine in HLikeGaLines])
print([[iLine, round(iLine.peak_energy, 1)] for iLine in HeLikeGaLines])
[SpectralLine: Ne10 2p 2P* J=3/2, np.float64(1022.0)]
[SpectralLine: O7 1s.2p 1P* J=1, np.float64(574.0)]
[[SpectralLine: Ga31 2p 2P* J=1/2, np.float64(9917.0)], [SpectralLine: Ga31 2s 2S J=1/2, np.float64(9918.0)], [SpectralLine: Ga31 2p 2P* J=3/2, np.float64(9960.3)], [SpectralLine: Ga31 3p 2P* J=1/2, np.float64(11767.7)], [SpectralLine: Ga31 3s 2S J=1/2, np.float64(11768.0)], [SpectralLine: Ga31 3d 2D J=3/2, np.float64(11780.5)]]
[[SpectralLine: Ga30 1s.2s 3S J=1, np.float64(9535.6)], [SpectralLine: Ga30 1s.2p 3P* J=0, np.float64(9571.8)], [SpectralLine: Ga30 1s.2p 3P* J=1, np.float64(9574.4)], [SpectralLine: Ga30 1s.2s 1S J=0, np.float64(9574.6)], [SpectralLine: Ga30 1s.2p 3P* J=2, np.float64(9607.4)], [SpectralLine: Ga30 1s.2p 1P* J=1, np.float64(9628.2)], [SpectralLine: Ga30 1s.3s 3S J=1, np.float64(11304.6)]]

HCI lines and models docstring info

hci_lines.py

Uses pickle file containing NIST ASD levels data to generate some commonly used HCI lines in mass. Meant to be a replacement for _highly_charged_ion_lines.py, which hard codes in line parameters.

The pickle file can be gzip-compressed, provided the compressed filename ends with ".gz".

February 2020 Paul Szypryt

NIST_ASD

Class for working with a pickled atomic spectra database

Source code in mass2/calibration/hci_lines.py
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
class NIST_ASD:
    """Class for working with a pickled atomic spectra database"""

    def __init__(self, pickleFilename: str | None = None):
        """Loads ASD pickle file (optionally gzipped)

        Parameters
        ----------
        pickleFilename : str | None, optional
            ASD pickle file name, as str, or if none then `mass2.calibration.hci_lines.DEFAULT_PICKLE_PATH` (default None)
        """

        if pickleFilename is None:
            pickleFilename = os.path.join(os.path.split(__file__)[0], str(DEFAULT_PICKLE_PATH))

        if pickleFilename.endswith(".gz"):
            with gzip.GzipFile(pickleFilename, "rb") as handle:
                self.NIST_ASD_Dict = pickle.load(handle)
        else:
            with open(pickleFilename, "rb") as handle:
                self.NIST_ASD_Dict = pickle.load(handle)

    def getAvailableElements(self):
        """Returns a list of all available elements from the ASD pickle file"""

        return list(self.NIST_ASD_Dict.keys())

    def getAvailableSpectralCharges(self, element: str) -> list[int]:
        """For a given element, returns a list of all available charge states from the ASD pickle file

        Parameters
        ----------
        element : str
            atomic symbol of element, e.g. 'Ne'

        Returns
        -------
        list[int]
            Available charge states
        """

        return list(self.NIST_ASD_Dict[element].keys())

    def getAvailableLevels(
        self,
        element: str,
        spectralCharge: int,
        requiredConf: str | None = None,
        requiredTerm: str | None = None,
        requiredJVal: str | None = None,
        maxLevels: int | None = None,
        units: str = "eV",
        getUncertainty: bool = True,
    ) -> dict:
        """For a given element and spectral charge state, return a dict of all known levels from the ASD pickle file

        Parameters
        ----------
        element : str
            Elemental atomic symbol, e.g. 'Ne'
        spectralCharge : int
            spectral charge state, e.g. 1 for neutral atoms, 10 for H-like Ne
        requiredConf : str | None, optional
            if not None, limits results to those with `conf == requiredConf`, by default None
        requiredTerm : str | None, optional
            if not None, limits results to those with `term == requiredTerm`, by default None
        requiredJVal : str | None, optional
            if not None, limits results to those with `a == requiredJVal`, by default None
        maxLevels : int | None, optional
            the maximum number of levels (sorted by energy) to return, by default None
        units : str, optional
            'cm-1' or 'eV' for returned line position. If 'eV', converts from database 'cm-1' values, by default "eV"
        getUncertainty : bool, optional
            whether to return uncertain values, by default True

        Returns
        -------
        dict
            A dictionary of energy level strings to energy levels.
        """
        if units not in {"eV", "cm-1"}:
            raise ValueError("Unit type not supported, please use eV or cm-1")

        spectralCharge = int(spectralCharge)
        levelsDict: dict = {}
        numLevels = 0
        for iLevel in list(self.NIST_ASD_Dict[element][spectralCharge].keys()):
            try:
                # Check to see if we reached maximum number of levels to return
                if maxLevels is not None:
                    if numLevels == maxLevels:
                        return levelsDict
                # If required, check to see if level matches search conf, term, JVal
                includeTerm = False
                includeJVal = False
                conf, term, j_str = iLevel.split()
                JVal = j_str.split("=")[1]
                includeConf = (requiredConf is None) or conf == requiredConf
                includeTerm = (requiredTerm is None) or term == requiredTerm
                includeJVal = (requiredJVal is None) or JVal == requiredJVal

                # Include levels that match, in either cm-1 or eV
                if includeConf and includeTerm and includeJVal:
                    numLevels += 1
                    if units == "cm-1":
                        if getUncertainty:
                            levelsDict[iLevel] = self.NIST_ASD_Dict[element][spectralCharge][iLevel]
                        else:
                            levelsDict[iLevel] = self.NIST_ASD_Dict[element][spectralCharge][iLevel][0]
                    elif units == "eV":
                        if getUncertainty:
                            levelsDict[iLevel] = [
                                iValue * INVCM_TO_EV for iValue in self.NIST_ASD_Dict[element][spectralCharge][iLevel]
                            ]
                        else:
                            levelsDict[iLevel] = INVCM_TO_EV * self.NIST_ASD_Dict[element][spectralCharge][iLevel][0]
            except ValueError:
                f"Warning: cannot parse level: {iLevel}"
        return levelsDict

    def getSingleLevel(
        self, element: str, spectralCharge: int, conf: str, term: str, JVal: str, units: str = "eV", getUncertainty: bool = True
    ) -> float:
        """Return the level data for a fully defined element, charge state, conf, term, and JVal.

        Parameters
        ----------
        element : str
            atomic symbol of element, e.g. 'Ne'
        spectralCharge : int
            spectral charge state, e.g. 1 for neutral atoms, 10 for H-like Ne
        conf : str
            nuclear configuration, e.g. '2p'
        term : str
            nuclear term, e.g. '2P*'
        JVal : str
            total angular momentum J, e.g. '3/2'
        units : str, optional
            'cm-1' or 'eV' for returned line position. If 'eV', converts from database 'cm-1' values, by default "eV"
        getUncertainty : bool, optional
            includes uncertainties in list of levels, by default True

        Returns
        -------
        float
            _description_
        """

        levelString = f"{conf} {term} J={JVal}"
        if units == "cm-1":
            if getUncertainty:
                levelEnergy = self.NIST_ASD_Dict[element][spectralCharge][levelString]
            else:
                levelEnergy = self.NIST_ASD_Dict[element][spectralCharge][levelString][0]
        elif units == "eV":
            if getUncertainty:
                levelEnergy = [iValue * INVCM_TO_EV for iValue in self.NIST_ASD_Dict[element][spectralCharge][levelString]]
            else:
                levelEnergy = self.NIST_ASD_Dict[element][spectralCharge][levelString][0] * INVCM_TO_EV
        else:
            raise ValueError("Unit type not supported, please use eV or cm-1")
        return levelEnergy

__init__(pickleFilename=None)

Loads ASD pickle file (optionally gzipped)

Parameters:
  • pickleFilename (str | None, default: None ) –

    ASD pickle file name, as str, or if none then mass2.calibration.hci_lines.DEFAULT_PICKLE_PATH (default None)

Source code in mass2/calibration/hci_lines.py
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
def __init__(self, pickleFilename: str | None = None):
    """Loads ASD pickle file (optionally gzipped)

    Parameters
    ----------
    pickleFilename : str | None, optional
        ASD pickle file name, as str, or if none then `mass2.calibration.hci_lines.DEFAULT_PICKLE_PATH` (default None)
    """

    if pickleFilename is None:
        pickleFilename = os.path.join(os.path.split(__file__)[0], str(DEFAULT_PICKLE_PATH))

    if pickleFilename.endswith(".gz"):
        with gzip.GzipFile(pickleFilename, "rb") as handle:
            self.NIST_ASD_Dict = pickle.load(handle)
    else:
        with open(pickleFilename, "rb") as handle:
            self.NIST_ASD_Dict = pickle.load(handle)

getAvailableElements()

Returns a list of all available elements from the ASD pickle file

Source code in mass2/calibration/hci_lines.py
50
51
52
53
def getAvailableElements(self):
    """Returns a list of all available elements from the ASD pickle file"""

    return list(self.NIST_ASD_Dict.keys())

getAvailableLevels(element, spectralCharge, requiredConf=None, requiredTerm=None, requiredJVal=None, maxLevels=None, units='eV', getUncertainty=True)

For a given element and spectral charge state, return a dict of all known levels from the ASD pickle file

Parameters:
  • element (str) –

    Elemental atomic symbol, e.g. 'Ne'

  • spectralCharge (int) –

    spectral charge state, e.g. 1 for neutral atoms, 10 for H-like Ne

  • requiredConf (str | None, default: None ) –

    if not None, limits results to those with conf == requiredConf, by default None

  • requiredTerm (str | None, default: None ) –

    if not None, limits results to those with term == requiredTerm, by default None

  • requiredJVal (str | None, default: None ) –

    if not None, limits results to those with a == requiredJVal, by default None

  • maxLevels (int | None, default: None ) –

    the maximum number of levels (sorted by energy) to return, by default None

  • units (str, default: 'eV' ) –

    'cm-1' or 'eV' for returned line position. If 'eV', converts from database 'cm-1' values, by default "eV"

  • getUncertainty (bool, default: True ) –

    whether to return uncertain values, by default True

Returns:
  • dict

    A dictionary of energy level strings to energy levels.

Source code in mass2/calibration/hci_lines.py
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
def getAvailableLevels(
    self,
    element: str,
    spectralCharge: int,
    requiredConf: str | None = None,
    requiredTerm: str | None = None,
    requiredJVal: str | None = None,
    maxLevels: int | None = None,
    units: str = "eV",
    getUncertainty: bool = True,
) -> dict:
    """For a given element and spectral charge state, return a dict of all known levels from the ASD pickle file

    Parameters
    ----------
    element : str
        Elemental atomic symbol, e.g. 'Ne'
    spectralCharge : int
        spectral charge state, e.g. 1 for neutral atoms, 10 for H-like Ne
    requiredConf : str | None, optional
        if not None, limits results to those with `conf == requiredConf`, by default None
    requiredTerm : str | None, optional
        if not None, limits results to those with `term == requiredTerm`, by default None
    requiredJVal : str | None, optional
        if not None, limits results to those with `a == requiredJVal`, by default None
    maxLevels : int | None, optional
        the maximum number of levels (sorted by energy) to return, by default None
    units : str, optional
        'cm-1' or 'eV' for returned line position. If 'eV', converts from database 'cm-1' values, by default "eV"
    getUncertainty : bool, optional
        whether to return uncertain values, by default True

    Returns
    -------
    dict
        A dictionary of energy level strings to energy levels.
    """
    if units not in {"eV", "cm-1"}:
        raise ValueError("Unit type not supported, please use eV or cm-1")

    spectralCharge = int(spectralCharge)
    levelsDict: dict = {}
    numLevels = 0
    for iLevel in list(self.NIST_ASD_Dict[element][spectralCharge].keys()):
        try:
            # Check to see if we reached maximum number of levels to return
            if maxLevels is not None:
                if numLevels == maxLevels:
                    return levelsDict
            # If required, check to see if level matches search conf, term, JVal
            includeTerm = False
            includeJVal = False
            conf, term, j_str = iLevel.split()
            JVal = j_str.split("=")[1]
            includeConf = (requiredConf is None) or conf == requiredConf
            includeTerm = (requiredTerm is None) or term == requiredTerm
            includeJVal = (requiredJVal is None) or JVal == requiredJVal

            # Include levels that match, in either cm-1 or eV
            if includeConf and includeTerm and includeJVal:
                numLevels += 1
                if units == "cm-1":
                    if getUncertainty:
                        levelsDict[iLevel] = self.NIST_ASD_Dict[element][spectralCharge][iLevel]
                    else:
                        levelsDict[iLevel] = self.NIST_ASD_Dict[element][spectralCharge][iLevel][0]
                elif units == "eV":
                    if getUncertainty:
                        levelsDict[iLevel] = [
                            iValue * INVCM_TO_EV for iValue in self.NIST_ASD_Dict[element][spectralCharge][iLevel]
                        ]
                    else:
                        levelsDict[iLevel] = INVCM_TO_EV * self.NIST_ASD_Dict[element][spectralCharge][iLevel][0]
        except ValueError:
            f"Warning: cannot parse level: {iLevel}"
    return levelsDict

getAvailableSpectralCharges(element)

For a given element, returns a list of all available charge states from the ASD pickle file

Parameters:
  • element (str) –

    atomic symbol of element, e.g. 'Ne'

Returns:
  • list[int]

    Available charge states

Source code in mass2/calibration/hci_lines.py
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
def getAvailableSpectralCharges(self, element: str) -> list[int]:
    """For a given element, returns a list of all available charge states from the ASD pickle file

    Parameters
    ----------
    element : str
        atomic symbol of element, e.g. 'Ne'

    Returns
    -------
    list[int]
        Available charge states
    """

    return list(self.NIST_ASD_Dict[element].keys())

getSingleLevel(element, spectralCharge, conf, term, JVal, units='eV', getUncertainty=True)

Return the level data for a fully defined element, charge state, conf, term, and JVal.

Parameters:
  • element (str) –

    atomic symbol of element, e.g. 'Ne'

  • spectralCharge (int) –

    spectral charge state, e.g. 1 for neutral atoms, 10 for H-like Ne

  • conf (str) –

    nuclear configuration, e.g. '2p'

  • term (str) –

    nuclear term, e.g. '2P*'

  • JVal (str) –

    total angular momentum J, e.g. '3/2'

  • units (str, default: 'eV' ) –

    'cm-1' or 'eV' for returned line position. If 'eV', converts from database 'cm-1' values, by default "eV"

  • getUncertainty (bool, default: True ) –

    includes uncertainties in list of levels, by default True

Returns:
  • float

    description

Source code in mass2/calibration/hci_lines.py
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
def getSingleLevel(
    self, element: str, spectralCharge: int, conf: str, term: str, JVal: str, units: str = "eV", getUncertainty: bool = True
) -> float:
    """Return the level data for a fully defined element, charge state, conf, term, and JVal.

    Parameters
    ----------
    element : str
        atomic symbol of element, e.g. 'Ne'
    spectralCharge : int
        spectral charge state, e.g. 1 for neutral atoms, 10 for H-like Ne
    conf : str
        nuclear configuration, e.g. '2p'
    term : str
        nuclear term, e.g. '2P*'
    JVal : str
        total angular momentum J, e.g. '3/2'
    units : str, optional
        'cm-1' or 'eV' for returned line position. If 'eV', converts from database 'cm-1' values, by default "eV"
    getUncertainty : bool, optional
        includes uncertainties in list of levels, by default True

    Returns
    -------
    float
        _description_
    """

    levelString = f"{conf} {term} J={JVal}"
    if units == "cm-1":
        if getUncertainty:
            levelEnergy = self.NIST_ASD_Dict[element][spectralCharge][levelString]
        else:
            levelEnergy = self.NIST_ASD_Dict[element][spectralCharge][levelString][0]
    elif units == "eV":
        if getUncertainty:
            levelEnergy = [iValue * INVCM_TO_EV for iValue in self.NIST_ASD_Dict[element][spectralCharge][levelString]]
        else:
            levelEnergy = self.NIST_ASD_Dict[element][spectralCharge][levelString][0] * INVCM_TO_EV
    else:
        raise ValueError("Unit type not supported, please use eV or cm-1")
    return levelEnergy

hci_models.py

Some useful methods for initializing GenericLineModel and CompositeMLEModel objects applied to HCI lines.

June 2020 Paul Szypryt

add_bg_model(generic_model, vary_slope=False)

Adds a LinearBackgroundModel to a generic lmfit model

Parameters:
  • generic_model (GenericLineModel) –

    object to which to add a linear background model

  • vary_slope (bool, default: False ) –

    allows a varying linear slope rather than just constant value, by default False

Returns:
  • GenericLineModel

    The input model, with background componets added

Source code in mass2/calibration/hci_models.py
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
def add_bg_model(generic_model: GenericLineModel, vary_slope: bool = False) -> GenericLineModel:
    """Adds a LinearBackgroundModel to a generic lmfit model

    Parameters
    ----------
    generic_model : GenericLineModel
        object to which to add a linear background model
    vary_slope : bool, optional
        allows a varying linear slope rather than just constant value, by default False

    Returns
    -------
    GenericLineModel
        The input model, with background componets added
    """
    # composite_name = generic_model._name
    # bg_prefix = f"{composite_name}_".replace(" ", "_").replace("J=", "").replace("/", "_").replace("*", "").replace(".", "")
    raise NotImplementedError("No LinearBackgroundModel still exists in mass2")

initialize_HLike_2P_model(element, conf, has_linear_background=False, has_tails=False, vary_amp_ratio=False)

Initializes H-like 2P models consisting of J=1/2 and J=3/2 lines

Parameters:
  • element (str) –

    atomic symbol as str, e.g. 'Ne' or 'Ar'

  • conf (str) –

    nuclear configuration as str, e.g. '2p' or '3p'

  • has_linear_background (bool, default: False ) –

    include a single linear background on top of the 2 Lorentzians, by default False

  • has_tails (bool, default: False ) –

    include low energy tail in the model, by default False

  • vary_amp_ratio (bool, default: False ) –

    allow the ratio of the J=3/2 to J=1/2 states to vary away from 2, by default False

Returns:
  • GenericLineModel

    The new composite line

Source code in mass2/calibration/hci_models.py
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
def initialize_HLike_2P_model(
    element: str, conf: str, has_linear_background: bool = False, has_tails: bool = False, vary_amp_ratio: bool = False
) -> GenericLineModel:
    """Initializes H-like 2P models consisting of J=1/2 and J=3/2 lines

    Parameters
    ----------
    element : str
        atomic symbol as str, e.g. 'Ne' or 'Ar'
    conf : str
        nuclear configuration as str, e.g. '2p' or '3p'
    has_linear_background : bool, optional
        include a single linear background on top of the 2 Lorentzians, by default False
    has_tails : bool, optional
        include low energy tail in the model, by default False
    vary_amp_ratio : bool, optional
        allow the ratio of the J=3/2 to J=1/2 states to vary away from 2, by default False

    Returns
    -------
    GenericLineModel
        The new composite line
    """

    # Set up line names and lmfit prefixes
    charge = int(xraydb.atomic_number(element))
    line_name_1_2 = f"{element}{charge} {conf} 2P* J=1/2"
    line_name_3_2 = f"{element}{charge} {conf} 2P* J=3/2"
    prefix_1_2 = f"{line_name_1_2}_".replace(" ", "_").replace("J=", "").replace("/", "_").replace("*", "").replace(".", "")
    prefix_3_2 = f"{line_name_3_2}_".replace(" ", "_").replace("J=", "").replace("/", "_").replace("*", "").replace(".", "")
    # Initialize individual lines and models
    line_1_2 = spectra[line_name_1_2]
    line_3_2 = spectra[line_name_3_2]
    model_1_2 = line_1_2.model(has_linear_background=False, has_tails=has_tails, prefix=prefix_1_2)
    model_3_2 = line_3_2.model(has_linear_background=False, has_tails=has_tails, prefix=prefix_3_2)
    # Initialize composite model and set addition H-like constraints
    composite_name = f"{element}{charge} {conf}"
    composite_model = initialize_hci_composite_model(
        composite_name=composite_name,
        individual_models=[model_1_2, model_3_2],
        has_linear_background=has_linear_background,
        peak_component_name=line_name_3_2,
    )
    amp_ratio_param_name = f"{element}{charge}_{conf}_amp_ratio"
    composite_model.set_param_hint(name=amp_ratio_param_name, value=0.5, min=0.0, vary=vary_amp_ratio)
    composite_model.set_param_hint(f"{prefix_1_2}integral", expr=f"{prefix_3_2}integral * {amp_ratio_param_name}")
    return composite_model

initialize_HeLike_complex_model(element, has_linear_background=False, has_tails=False, additional_line_names=[])

Initializes 1s2s,2p He-like complexes for a given element.

By default, uses only the 1s.2s 3S J=1, 1s.2p 3P J=1, and 1s.2p 1P J=1 lines.

Parameters:
  • element (str) –

    atomic symbol as str, e.g. 'Ne' or 'Ar'

  • has_linear_background (bool, default: False ) –

    include a single linear background on top of the Lorentzian models, by default False

  • has_tails (bool, default: False ) –

    include low energy tail in the model, by default False

  • additional_line_names (list, default: [] ) –

    additional line names to include in model, e.g. low level Li/Be-like features, by default []

Returns:
  • GenericLineModel

    A model of the given HCI complex.

Source code in mass2/calibration/hci_models.py
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
def initialize_HeLike_complex_model(
    element: str, has_linear_background: bool = False, has_tails: bool = False, additional_line_names: list = []
) -> GenericLineModel:
    """Initializes 1s2s,2p He-like complexes for a given element.

    By default, uses only the 1s.2s 3S J=1, 1s.2p 3P* J=1, and 1s.2p 1P* J=1 lines.

    Parameters
    ----------
    element : str
        atomic symbol as str, e.g. 'Ne' or 'Ar'
    has_linear_background : bool, optional
        include a single linear background on top of the Lorentzian models, by default False
    has_tails : bool, optional
        include low energy tail in the model, by default False
    additional_line_names : list, optional
        additional line names to include in model, e.g. low level Li/Be-like features, by default []

    Returns
    -------
    GenericLineModel
        A model of the given HCI complex.
    """

    # Set up line names
    charge = int(xraydb.atomic_number(element) - 1)
    line_name_1s2s_3S = f"{element}{charge} 1s.2s 3S J=1"
    line_name_1s2p_3P = f"{element}{charge} 1s.2p 3P* J=1"
    line_name_1s2p_1P = f"{element}{charge} 1s.2p 1P* J=1"
    line_names = np.hstack([[line_name_1s2s_3S, line_name_1s2p_3P, line_name_1s2p_1P], additional_line_names])
    # Set up lines and models based on line_names
    # individual_lines = [spectra[i_line_name]() for i_line_name in line_names]
    individual_models = [
        initialize_hci_line_model(i_line_name, has_linear_background=False, has_tails=has_tails) for i_line_name in line_names
    ]
    # Set up composite model
    composite_name = f"{element}{charge} 1s2s_2p Complex"
    composite_model = initialize_hci_composite_model(
        composite_name=composite_name,
        individual_models=individual_models,
        has_linear_background=has_linear_background,
        peak_component_name=line_name_1s2p_1P,
    )
    return composite_model

initialize_hci_composite_model(composite_name, individual_models, has_linear_background=False, peak_component_name=None)

Initializes composite lmfit model from the sum of input models

Parameters:
  • composite_name (str) –

    name given to composite line model

  • individual_models (list[GenericLineModel]) –

    Models to sum into a composite

  • has_linear_background (bool, default: False ) –

    include a single linear background on top of group of lorentzians, by default False

  • peak_component_name (str | None, default: None ) –

    designate a component to be a peak for energy, all expressions are referenced to this component, by default None

Returns:
  • GenericLineModel

    The new composite line

Source code in mass2/calibration/hci_models.py
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
def initialize_hci_composite_model(
    composite_name: str,
    individual_models: list[GenericLineModel],
    has_linear_background: bool = False,
    peak_component_name: str | None = None,
) -> GenericLineModel:
    """Initializes composite lmfit model from the sum of input models

    Parameters
    ----------
    composite_name : str
        name given to composite line model
    individual_models : list[GenericLineModel]
        Models to sum into a composite
    has_linear_background : bool, optional
        include a single linear background on top of group of lorentzians, by default False
    peak_component_name : str | None, optional
        designate a component to be a peak for energy, all expressions are referenced to this component, by default None

    Returns
    -------
    GenericLineModel
        The new composite line
    """

    composite_model: GenericLineModel = np.sum(individual_models)
    composite_model.name = composite_name
    if has_linear_background:
        composite_model = add_bg_model(composite_model)
    # Workaround for energy calibration using composite models, pick 1st GenericLineModel component
    line_model_components = [
        i_comp for i_comp in composite_model.components if isinstance(i_comp, mass2.calibration.line_models.GenericLineModel)
    ]
    if peak_component_name is None:
        peak_component_name = line_model_components[0]._name
    peak_component_index = [i_comp._name for i_comp in line_model_components].index(peak_component_name)
    peak_component = line_model_components[peak_component_index]
    composite_model.peak_prefix = peak_component.prefix
    composite_model.peak_energy = peak_component.spect.peak_energy
    # Set up some constraints relative to peak_component
    num_line_components = len(line_model_components)
    line_component_prefixes = [iComp.prefix for iComp in line_model_components]
    line_component_energies = [iComp.spect.peak_energy for iComp in line_model_components]
    for i in np.arange(num_line_components):
        if i != peak_component_index:
            # Single fwhm across model
            composite_model.set_param_hint(f"{line_component_prefixes[i]}fwhm", expr=f"{composite_model.peak_prefix}fwhm")
            # Single dph_de across model
            composite_model.set_param_hint(f"{line_component_prefixes[i]}dph_de", expr=f"{composite_model.peak_prefix}dph_de")
            # Fixed energy separation based on database values
            separation = line_component_energies[i] - composite_model.peak_energy
            hint = f"({separation} * {composite_model.peak_prefix}dph_de) + {composite_model.peak_prefix}peak_ph"
            composite_model.set_param_hint(f"{line_component_prefixes[i]}peak_ph", expr=hint)
    composite_model.shortname = composite_name
    return composite_model

initialize_hci_line_model(line_name, has_linear_background=False, has_tails=False)

Initializes a single lorentzian HCI line model. Reformats line_name to create a lmfit valid prefix.

Parameters:
  • line_name (str) –

    name of line to use in mass2.spectra

  • has_linear_background (bool, default: False ) –

    include linear background in the model, by default False

  • has_tails (bool, default: False ) –

    include low-energy tail in the model, by default False

Returns:
  • GenericLineModel

    New HCI line.

Source code in mass2/calibration/hci_models.py
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
def initialize_hci_line_model(line_name: str, has_linear_background: bool = False, has_tails: bool = False) -> GenericLineModel:
    """Initializes a single lorentzian HCI line model. Reformats line_name to create a lmfit valid prefix.

    Parameters
    ----------
    line_name : str
        name of line to use in mass2.spectra
    has_linear_background : bool, optional
        include linear background in the model, by default False
    has_tails : bool, optional
        include low-energy tail in the model, by default False

    Returns
    -------
    GenericLineModel
        New HCI line.
    """
    line = spectra[line_name]
    prefix = f"{line_name}_".replace(" ", "_").replace("J=", "").replace("/", "_").replace("*", "").replace(".", "")
    line_model = line.model(has_linear_background=has_linear_background, has_tails=has_tails, prefix=prefix)
    line_model.shortname = line_name
    return line_model

models(has_linear_background=False, has_tails=False, vary_Hlike_amp_ratio=False, additional_Helike_complex_lines=[])

Generates some commonly used HCI line models that can be used for energy calibration, etc.

Parameters:
  • has_linear_background (bool, default: False ) –

    include a single linear background on top of the 2 Lorentzians, by default False

  • has_tails (bool, default: False ) –

    nclude low-energy tail in the model, by default False

  • vary_Hlike_amp_ratio (bool, default: False ) –

    allow the ratio of the J=3/2 to J=1/2 H-like states to vary, by default False

  • additional_Helike_complex_lines (list, default: [] ) –

    additional line names to include inHe-like complex model, e.g. low level Li/Be-like features, by default []

Returns:
  • _type_

    Dictionary of mass2.calibration.fluorescence_lines.SpectralLine objects, containing commonly used HCI lines.

Source code in mass2/calibration/hci_models.py
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
def models(
    has_linear_background: bool = False,
    has_tails: bool = False,
    vary_Hlike_amp_ratio: bool = False,
    additional_Helike_complex_lines=[],
) -> dict:
    """Generates some commonly used HCI line models that can be used for energy calibration, etc.

    Parameters
    ----------
    has_linear_background : bool, optional
        include a single linear background on top of the 2 Lorentzians, by default False
    has_tails : bool, optional
        nclude low-energy tail in the model, by default False
    vary_Hlike_amp_ratio : bool, optional
        allow the ratio of the J=3/2 to J=1/2 H-like states to vary, by default False
    additional_Helike_complex_lines : list, optional
        additional line names to include inHe-like complex model, e.g. low level Li/Be-like features, by default []

    Returns
    -------
    _type_
        Dictionary of mass2.calibration.fluorescence_lines.SpectralLine objects, containing commonly used HCI lines.
    """

    models_dict = {}
    # Make some common H-like 2P* models
    conf_Hlike_2P_dict = {}
    conf_Hlike_2P_dict["N"] = ["3p", "4p", "5p"]
    conf_Hlike_2P_dict["O"] = ["3p", "4p", "5p"]
    conf_Hlike_2P_dict["Ne"] = ["2p", "3p", "4p", "5p"]
    conf_Hlike_2P_dict["Ar"] = ["2p", "3p", "4p", "5p"]
    for i_element in list(conf_Hlike_2P_dict.keys()):
        for i_conf in conf_Hlike_2P_dict[i_element]:
            Hlike_model = initialize_HLike_2P_model(
                i_element,
                i_conf,
                has_linear_background=has_linear_background,
                has_tails=has_tails,
                vary_amp_ratio=vary_Hlike_amp_ratio,
            )
            models_dict[Hlike_model._name] = Hlike_model

    # Make some common He-like 1s2s,2p complex and higher order 1p* models
    # He-like lines
    Helike_complex_elements = ["N", "O", "Ne", "Ar"]
    for i_element in Helike_complex_elements:
        Helike_model = initialize_HeLike_complex_model(
            i_element,
            has_linear_background=has_linear_background,
            has_tails=has_tails,
            additional_line_names=additional_Helike_complex_lines,
        )
        models_dict[Helike_model._name] = Helike_model
    # 1s.np 1P* lines for n>=3
    conf_Helike_1P_dict = {}
    conf_Helike_1P_dict["N"] = ["1s.4p", "1s.5p"]
    conf_Helike_1P_dict["O"] = ["1s.4p", "1s.5p"]
    conf_Helike_1P_dict["Ne"] = ["1s.3p", "1s.4p", "1s.5p"]
    conf_Helike_1P_dict["Ar"] = ["1s.3p", "1s.4p", "1s.5p"]
    for i_element in list(conf_Helike_1P_dict.keys()):
        i_charge = int(xraydb.atomic_number(i_element) - 1)
        for i_conf in conf_Helike_1P_dict[i_element]:
            Helike_line_name = f"{i_element}{i_charge} {i_conf} 1P* J=1"
            Helike_model = initialize_hci_line_model(
                Helike_line_name, has_linear_background=has_linear_background, has_tails=has_tails
            )
            models_dict[Helike_model._name] = Helike_model

    # Some more complicated cases
    # 500 eV region of H-/He-like N
    N6_1s3p_model = initialize_hci_line_model("N6 1s.3p 1P* J=1", has_linear_background=False, has_tails=has_tails)
    N7_2p_model = initialize_HLike_2P_model(
        "N", "2p", has_linear_background=False, has_tails=has_tails, vary_amp_ratio=vary_Hlike_amp_ratio
    )
    N_500eV_model = initialize_hci_composite_model(
        "N 500eV Region",
        [N6_1s3p_model, N7_2p_model],
        has_linear_background=has_linear_background,
        peak_component_name="N7 2p 2P* J=3/2",
    )
    models_dict[N_500eV_model._name] = N_500eV_model
    # 660 eV region of H-/He-like O
    O8_2p_model = initialize_HLike_2P_model(
        "O", "2p", has_linear_background=False, has_tails=has_tails, vary_amp_ratio=vary_Hlike_amp_ratio
    )
    O7_1s3p_model = initialize_hci_line_model("O7 1s.3p 1P* J=1", has_linear_background=False, has_tails=has_tails)
    O_660eV_model = initialize_hci_composite_model(
        "O 660eV Region",
        [O8_2p_model, O7_1s3p_model],
        has_linear_background=has_linear_background,
        peak_component_name="O8 2p 2P* J=3/2",
    )
    models_dict[O_660eV_model._name] = O_660eV_model

    return models_dict