'''
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.
February 2020
Paul Szypryt
'''
import numpy as np
import pickle
import scipy.constants as sp_const
import os
from . import fluorescence_lines
from . import line_models
from . import LORENTZIAN_PEAK_HEIGHT
import xraydb
INVCM_TO_EV = sp_const.c * sp_const.physical_constants['Planck constant in eV s'][0] * 100.0
DEFAULT_PICKLE_NAME = 'nist_asd.pickle'
[docs]
class NIST_ASD:
'''Class for working with a pickled atomic spectra database'''
def __init__(self, pickleFilename=None):
'''Loads ASD pickle file
Args:
pickleFilename: (default None) ASD pickle file name, as str, if not using default
'''
if pickleFilename is None:
pickleFilename = os.path.join(os.path.split(__file__)[0], DEFAULT_PICKLE_NAME)
with open(pickleFilename, 'rb') as handle:
self.NIST_ASD_Dict = pickle.load(handle)
[docs]
def getAvailableElements(self):
'''Returns a list of all available elements from the ASD pickle file'''
return list(self.NIST_ASD_Dict.keys())
[docs]
def getAvailableSpectralCharges(self, element):
'''For a given element, returns a list of all available charge states from the ASD pickle file
Args:
element: str representing atomic symbol of element, e.g. 'Ne'
'''
return list(self.NIST_ASD_Dict[element].keys())
[docs]
def getAvailableLevels(self, 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
Args:
element: str representing atomic symbol of element, e.g. 'Ne'
spectralCharge: int representing spectral charge state, e.g. 1 for neutral atoms, 10 for H-like Ne
requiredConf: (default None) filters results to those with ``conf == requiredConf``
requiredTerm: (default None) filters results to those with ``term == requiredTerm``
requiredJVal: (default None) filters results to those with ``JVal == requiredJVal``
maxLevels: (default None) the maximum number of levels (sorted by energy) to return
units: (default 'eV') 'cm-1' or 'eV' for returned line position. If 'eV', converts from database 'cm-1' values
'''
spectralCharge = int(spectralCharge)
levelsDict = {}
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
includeConf = False
includeTerm = False
includeJVal = False
conf, term, j_str = iLevel.split()
JVal = j_str.split('=')[1]
if requiredConf is None:
includeConf = True
elif conf == requiredConf:
includeConf = True
if requiredTerm is None:
includeTerm = True
elif term == requiredTerm:
includeTerm = True
if requiredJVal is None:
includeJVal = True
elif JVal == requiredJVal:
includeJVal = True
# 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]
else:
levelsDict = None
print('Unit type not supported, please use eV or cm-1')
except ValueError:
f'Warning: cannot parse level: {iLevel}'
return levelsDict
[docs]
def getSingleLevel(self, element, spectralCharge, conf, term, JVal, units='eV', getUncertainty=True):
'''Return the level data for a fully defined element, charge state, conf, term, and JVal.
Args:
element: str representing atomic symbol of element, e.g. 'Ne'
spectralCharge: int representing spectral charge state, e.g. 1 for neutral atoms, 10 for H-like Ne
conf: str representing nuclear configuration, e.g. '2p'
term: str representing nuclear term, e.g. '2P*'
JVal: str representing total angular momentum J, e.g. '3/2'
units: (default 'eV') 'cm-1' or 'eV' for returned line position. If 'eV', converts from database 'cm-1' values
getUncertainty: (default True) if True, includes uncertainties in list of levels
'''
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:
levelEnergy = None
print('Unit type not supported, please use eV or cm-1')
return levelEnergy
# Some non-class functions useful for integration with mass
def add_hci_line(element, spectr_ch, line_identifier, energies, widths, ratios, nominal_peak_energy=None):
energies = np.array(energies)
widths = np.array(widths)
ratios = np.array(ratios)
if nominal_peak_energy is None:
nominal_peak_energy = np.dot(energies, ratios) / np.sum(ratios)
linetype = f"{int(spectr_ch)} {line_identifier}"
spectrum_class = fluorescence_lines.addline(
element=element,
material="Highly Charged Ion",
linetype=linetype,
reference_short='NIST ASD',
fitter_type=line_models.GenericLineModel,
reference_plot_instrument_gaussian_fwhm=0.5,
nominal_peak_energy=nominal_peak_energy,
energies=energies,
lorentzian_fwhm=widths,
reference_amplitude=ratios,
reference_amplitude_type=LORENTZIAN_PEAK_HEIGHT,
ka12_energy_diff=None
)
return spectrum_class
def add_H_like_lines_from_asd(asd, element, maxLevels=None):
spectr_ch = xraydb.atomic_number(element)
added_lines = []
if maxLevels is not None:
levelsDict = asd.getAvailableLevels(
element, spectralCharge=spectr_ch, maxLevels=maxLevels + 1)
else:
levelsDict = asd.getAvailableLevels(element, spectralCharge=spectr_ch)
for iLevel in list(levelsDict.keys()):
lineEnergy = levelsDict[iLevel][0]
if lineEnergy != 0.0:
iLine = add_hci_line(element=element, spectr_ch=spectr_ch, line_identifier=iLevel, energies=[
lineEnergy], widths=[0.1], ratios=[1.0])
added_lines.append(iLine)
return added_lines
def add_He_like_lines_from_asd(asd, element, maxLevels=None):
spectr_ch = xraydb.atomic_number(element) - 1
added_lines = []
if maxLevels is not None:
levelsDict = asd.getAvailableLevels(
element, spectralCharge=spectr_ch, maxLevels=maxLevels + 1)
else:
levelsDict = asd.getAvailableLevels(element, spectralCharge=spectr_ch)
for iLevel in list(levelsDict.keys()):
lineEnergy = levelsDict[iLevel][0]
if lineEnergy != 0.0:
iLine = add_hci_line(element=element, spectr_ch=spectr_ch, line_identifier=iLevel, energies=[
lineEnergy], widths=[0.1], ratios=[1.0])
added_lines.append(iLine)
return added_lines
# Script for adding some lines for elements commonly used at the EBIT
asd = NIST_ASD()
elementList = ['N', 'O', 'Ne', 'Ar']
# Add all known H- and He-like lines for these elements
for iElement in elementList:
add_H_like_lines_from_asd(asd=asd, element=iElement, maxLevels=None)
add_He_like_lines_from_asd(asd=asd, element=iElement, maxLevels=None)