from astropy.table import Table
from scipy.interpolate import interp1d
import numpy as np
import matplotlib.pyplot as plt
import copy
import os
from spectractor import parameters
from spectractor.config import set_logger
from spectractor.tools import gauss, multigauss_and_bgd, rescale_x_to_legendre, plot_spectrum_simple
[docs]
class Line:
"""Class modeling the emission or absorption lines."""
def __init__(self, wavelength, label, atmospheric=False, emission=False, label_pos=[0.007, 0.02],
width_bounds=[1, 6], use_for_calibration=False):
"""Class modeling the emission or absorption lines. lines attributes contains main spectral lines
sorted in wavelength.
Parameters
----------
wavelength: float
Wavelength of the spectral line in nm
label: str
atmospheric: bool
Set True if the spectral line is atmospheric (default: False)
emission: bool
Set True if the spectral line has to be detected in emission. Can't be true if the line is atmospheric.
(default: False)
label_pos: [float, float]
Position of the label in the plot with respect to the vertical lin (default: [0.007,0.02])
width_bounds: [float, float]
Minimum and maximum width (in nm) of the line for fitting procedures (default: [1,7])
use_for_calibration: bool
Use this line for the dispersion relation calibration, bright line recommended (default: False)
Examples
--------
>>> l = Line(550, label='test', atmospheric=True, emission=True)
>>> print(l.wavelength)
550
>>> print(l.label)
test
>>> print(l.atmospheric)
True
>>> print(l.emission)
False
"""
self.my_logger = set_logger(self.__class__.__name__)
self.wavelength = wavelength # in nm
self.label = label
self.label_pos = label_pos
self.atmospheric = atmospheric
self.emission = emission
if self.atmospheric:
self.emission = False
self.width_bounds = width_bounds
self.fitted = False
self.use_for_calibration = use_for_calibration
self.high_snr = False
self.fit_lambdas = None
self.fit_gauss = None
self.fit_bgd = None
self.fit_snr = None
self.fit_fwhm = None
self.fit_popt = None
self.fit_pcov = None
self.fit_popt_gaussian = None
self.fit_pcov_gaussian = None
self.fit_chisq = None
self.fit_eqwidth_mod = None
self.fit_eqwidth_data = None
self.fit_bgd_npar = parameters.CALIB_BGD_NPARAMS
[docs]
def gaussian_model(self, lambdas, A=1, sigma=2, use_fit=False):
"""Return a Gaussian model of the spectral line.
Parameters
----------
lambdas: float, array
Wavelength array of float in nm
A: float
Amplitude of the Gaussian (default: +1)
sigma: float
Standard deviation of the Gaussian (default: 2)
use_fit: bool, optional
If True, it overrides the previous setting values and use the Gaussian fit made on data, if ti exists.
Returns
-------
model: float, array
The amplitude array of float of the Gaussian model of the line.
Examples
--------
Give lambdas as a float:
>>> l = Line(656.3, atmospheric=False, label='$H\\alpha$')
>>> sigma = 2.
>>> model = l.gaussian_model(656.3, A=1, sigma=sigma, use_fit=False)
>>> print(model)
1.0
>>> model = l.gaussian_model(656.3+sigma*np.sqrt(2*np.log(2)), A=1, sigma=sigma, use_fit=False)
>>> print(f"{model:.4f}")
0.5000
Use a fit (for the example we create a mock fit result):
>>> l.fit_lambdas = np.arange(600,700,2)
>>> l.fit_gauss = gauss(l.fit_lambdas, 1e-10, 650, 2.3)
>>> l.fit_fwhm = 2.3*2*np.sqrt(2*np.log(2))
>>> lambdas = np.arange(500,1000,1)
>>> model = l.gaussian_model(lambdas, A=1, sigma=sigma, use_fit=True)
>>> print(model[:5])
[0. 0. 0. 0. 0.]
"""
if use_fit and self.fit_gauss is not None:
interp = interp1d(self.fit_lambdas, self.fit_gauss, bounds_error=False, fill_value=0.)
return interp(lambdas)
else:
return gauss(lambdas, A=A, x0=self.wavelength, sigma=sigma)
[docs]
class Lines:
"""Class gathering all the lines and associated methods."""
def __init__(self, lines, redshift=0, atmospheric_lines=True, hydrogen_only=False, emission_spectrum=False,
orders=[1]):
""" Main emission/absorption lines in nm. Sorted lines are sorted in self.lines.
See http://www.pa.uky.edu/~peter/atomic/ or https://physics.nist.gov/PhysRefData/ASD/lines_form.html
Parameters
----------
lines: list
List of Line objects to gather and sort.
redshift: float, optional
Red shift the spectral lines. Must be positive or null (default: 0)
atmospheric_lines: bool, optional
Set True if the atmospheric spectral lines must be included (default: True)
hydrogen_only: bool, optional
Set True to gather only the hydrogen spectral lines, atmospheric lines still included (default: False)
emission_spectrum: bool, optional
Set True if the spectral line has to be detected in emission (default: False)
orders: list, optional
List of integers corresponding to the diffraction order to account for the line search and the
wavelength calibration (default: [1])
Examples
--------
The default first five lines:
>>> lines = Lines(ISM_LINES+HYDROGEN_LINES, redshift=0, atmospheric_lines=False, hydrogen_only=False, emission_spectrum=False)
>>> print([lines.lines[i].wavelength for i in range(5)])
[353.1, 375.0, 377.1, 379.8, 383.5]
The four hydrogen lines only:
>>> lines = Lines(ISM_LINES+HYDROGEN_LINES+ATMOSPHERIC_LINES, redshift=0, atmospheric_lines=False, hydrogen_only=True, emission_spectrum=True)
>>> print([lines.lines[i].wavelength for i in range(4)])
[397.0, 410.2, 434.0, 486.3]
>>> print(lines.emission_spectrum)
True
Redshift the hydrogen lines, the atmospheric lines stay unchanged:
>>> lines = Lines(ISM_LINES+HYDROGEN_LINES+ATMOSPHERIC_LINES, redshift=1, atmospheric_lines=True, hydrogen_only=True, emission_spectrum=True)
>>> print([lines.lines[i].wavelength for i in range(7)])
[687.472, 760.3, 763.1, 794.0, 820.4, 822.696, 868.0]
Redshift all the spectral lines, except the atmospheric lines:
>>> lines = Lines(ISM_LINES+HYDROGEN_LINES+ATMOSPHERIC_LINES, redshift=1, atmospheric_lines=True, hydrogen_only=False, emission_spectrum=True)
>>> print([lines.lines[i].wavelength for i in range(5)])
[687.472, 706.2, 750.0, 754.2, 759.6]
Hydrogen lines at order 1 and 2:
>>> lines = Lines(HYDROGEN_LINES, redshift=0, atmospheric_lines=True, hydrogen_only=False, emission_spectrum=True, orders=[1, 2])
>>> print([lines.lines[i].wavelength for i in range(len(lines.lines))])
[375.0, 377.1, 379.8, 383.5, 388.9, 397.0, 410.2, 434.0, 486.3, 656.3, 750.0, 754.2, 759.6, 767.0, 777.8, 794.0, 820.4, 868.0, 972.6, 1312.6]
Negative redshift:
>>> lines = Lines(HYDROGEN_LINES, redshift=-0.5)
"""
self.my_logger = set_logger(self.__class__.__name__)
if redshift < -1e-2:
self.my_logger.error(f'\n\tRedshift must small in absolute value (|z|<0.01) or be positive or null. '
f'Got redshift={redshift}.')
self.lines = []
self.table = Table()
self.orders = orders
for order in orders:
for line in lines:
tmp_line = copy.deepcopy(line)
tmp_line.wavelength *= order
if order > 1:
if line.label[-1] == "$":
tmp_line.label = tmp_line.label[:-1]
tmp_line.label += "^(2)"
if line.label[-1] == "$":
tmp_line.label += "$"
self.lines.append(tmp_line)
self.redshift = redshift
self.atmospheric_lines = atmospheric_lines
self.hydrogen_only = hydrogen_only
self.emission_spectrum = emission_spectrum
self.lines = self.sort_lines()
[docs]
def sort_lines(self):
"""Sort the lines in increasing order of wavelength, and add the redshift effect.
Returns
-------
sorted_lines: list
List of the sorted lines
Examples
--------
>>> lines = Lines(HYDROGEN_LINES+ATMOSPHERIC_LINES, redshift=0)
>>> sorted_lines = lines.sort_lines()
>>> print([l.wavelength for l in sorted_lines][:5])
[375.0, 377.1, 379.8, 383.5, 388.9]
"""
sorted_lines = []
import copy
for line in self.lines:
if self.hydrogen_only:
if not self.atmospheric_lines:
if line.atmospheric:
continue
if '$H\\' not in line.label:
continue
else:
if not line.atmospheric and '$H\\' not in line.label:
continue
else:
if not self.atmospheric_lines and line.atmospheric:
continue
sorted_lines.append(copy.copy(line))
if self.redshift > 0:
for line in sorted_lines:
if not line.atmospheric:
line.wavelength *= (1 + self.redshift)
sorted_lines = sorted(sorted_lines, key=lambda x: x.wavelength)
return sorted_lines
[docs]
def plot_atomic_lines(self, ax, color_atomic='g', color_atmospheric='b', fontsize=12, force=False,
calibration_only=False):
"""Over plot the atomic lines as vertical lines, only if they are fitted or with high
signal-to-noise ratio, unless force keyword is set to True.
Parameters
----------
ax: Axes
An Axes instance on which plot the spectral lines.
color_atomic: str
Color of the atomic lines (default: 'g').
color_atmospheric: str
Color of the atmospheric lines (default: 'b').
fontsize: int
Font size of the spectral line labels (default: 12).
force: bool
Force the plot of vertical lines if set to True even if they are not detected (default: False).
calibration_only: bool
Plot only the lines used for calibration if True (default: False).
Examples
--------
>>> import matplotlib.pyplot as plt
>>> f, ax = plt.subplots(1,1)
>>> ax.set_xlim(300,1000) # doctest: +ELLIPSIS
(300..., 1000...)
>>> lines = Lines(HYDROGEN_LINES+ATMOSPHERIC_LINES)
>>> lines.lines[5].fitted = True
>>> lines.lines[5].high_snr = True
>>> lines.lines[-1].fitted = True
>>> lines.lines[-1].high_snr = True
>>> ax = lines.plot_atomic_lines(ax)
>>> if parameters.DISPLAY: plt.show()
.. doctest::
:hide:
>>> assert ax is not None
.. plot::
import matplotlib.pyplot as plt
import numpy as np
from spectractor.extractor.spectroscopy import *
f, ax = plt.subplots(1,1)
ax.set_xlim(300,1000)
lines = Lines(HYDROGEN_LINES+ATMOSPHERIC_LINES)
lines.lines[5].fitted = True
lines.lines[5].high_snr = True
lines.lines[-1].fitted = True
lines.lines[-1].high_snr = True
ax = lines.plot_atomic_lines(ax)
plt.show()
"""
xlim = ax.get_xlim()
for line in self.lines:
if (not line.fitted or not line.high_snr) and not force:
continue
if not line.use_for_calibration and calibration_only:
continue
color = color_atomic
if line.atmospheric:
color = color_atmospheric
ax.axvline(line.wavelength, lw=2, color=color)
xpos = (line.wavelength - xlim[0]) / (xlim[1] - xlim[0]) + line.label_pos[0]
if 0 < xpos < 1:
ax.annotate(line.label, xy=(xpos, line.label_pos[1]), rotation=90, ha='left', va='bottom',
xycoords='axes fraction', color=color, fontsize=fontsize)
return ax
[docs]
def plot_detected_lines(self, ax=None, calibration_only=False):
"""Overplot the fitted lines on a spectrum.
Parameters
----------
ax: Axes
The Axes instance if needed (default: None).
calibration_only: bool
Plot only the lines used for calibration if True (default: False).
Examples
--------
Creation of a mock spectrum with emission and absorption lines:
>>> from spectractor.extractor.spectrum import Spectrum, detect_lines
>>> lambdas = np.arange(300,1000,1)
>>> spectrum = 1e4*np.exp(-((lambdas-600)/200)**2)
>>> spectrum += HALPHA.gaussian_model(lambdas, A=5000, sigma=3)
>>> spectrum += HBETA.gaussian_model(lambdas, A=3000, sigma=2)
>>> spectrum += O2_1.gaussian_model(lambdas, A=-3000, sigma=7)
>>> spectrum_err = np.sqrt(spectrum)
>>> spec = Spectrum()
>>> spec.lambdas = lambdas
>>> spec.data = spectrum
>>> spec.err = spectrum_err
>>> fwhm_func = interp1d(lambdas, 0.01 * lambdas)
Detect the lines:
>>> lines = Lines([HALPHA, HBETA, O2_1], hydrogen_only=True,
... atmospheric_lines=True, redshift=0, emission_spectrum=True)
>>> global_chisq, res, ws = detect_lines(lines, lambdas, spectrum, spectrum_err, fwhm_func=fwhm_func)
.. doctest::
:hide:
>>> assert(global_chisq < 1.)
Plot the result:
>>> spec.lines = lines
>>> fig = plt.figure()
>>> plot_spectrum_simple(plt.gca(), lambdas, spec.data, data_err=spec.err)
>>> lines.plot_detected_lines(plt.gca())
>>> plt.show()
.. plot::
import matplotlib.pyplot as plt
from spectractor.tools import plot_spectrum_simple
from spectractor.extractor.spectrum import Spectrum, detect_lines
from spectractor.extractor.spectroscopy import *
lambdas = np.arange(300,1000,1)
spectrum = 1e4*np.exp(-((lambdas-600)/200)**2)
spectrum += HALPHA.gaussian_model(lambdas, A=5000, sigma=3)
spectrum += HBETA.gaussian_model(lambdas, A=3000, sigma=2)
spectrum += O2.gaussian_model(lambdas, A=-3000, sigma=7)
spectrum_err = np.sqrt(spectrum)
spec = Spectrum()
spec.lambdas = lambdas
spec.data = spectrum
spec.err = spectrum_err
fwhm_func = interp1d(lambdas, 0.01 * lambdas)
lines = Lines([HALPHA, HBETA, O2], hydrogen_only=True, atmospheric_lines=True, redshift=0, emission_spectrum=True)
global_chisq, res, ws = detect_lines(lines, lambdas, spectrum, spectrum_err, fwhm_func=fwhm_func)
spec.lines = lines
fig = plt.figure()
plot_spectrum_simple(plt.gca(), lambdas, spec.data, data_err=spec.err)
lines.plot_detected_lines(plt.gca())
plt.show()
"""
lambdas = np.zeros(1)
for line in self.lines:
if not line.use_for_calibration and calibration_only:
continue
if line.fitted is True:
# look for lines in subset fit
bgd_npar = line.fit_bgd_npar
parameters.CALIB_BGD_NPARAMS = bgd_npar
if lambdas.shape != line.fit_lambdas.shape or not np.allclose(lambdas, line.fit_lambdas, 1e-3):
lambdas = np.copy(line.fit_lambdas)
if ax is not None:
x_norm = rescale_x_to_legendre(lambdas)
ax.plot(lambdas, multigauss_and_bgd(np.array([x_norm, lambdas]), *line.fit_popt), lw=2, color='b')
bgd = np.polynomial.legendre.legval(x_norm, line.fit_popt[0:bgd_npar])
# bgd = np.polyval(line.fit_popt[0:bgd_npar], lambdas)
ax.plot(lambdas, bgd, lw=2, color='b', linestyle='--')
[docs]
def build_detected_line_table(self, amplitude_units="", calibration_only=False):
"""Build the detected line on screen as an Astropy table.
Parameters
----------
amplitude_units: str, optional
Units of the line amplitude (default: "").
calibration_only: bool
Include only the lines used for calibration if True (default: False).
Returns
-------
table: Table
Astropy table containing the main line characteristics.
Examples
--------
Creation of a mock spectrum with emission and absorption lines
>>> from spectractor.extractor.spectrum import Spectrum, detect_lines
>>> lambdas = np.arange(300,1000,1)
>>> spectrum = 1e4*np.exp(-((lambdas-600)/200)**2)
>>> spectrum += HALPHA.gaussian_model(lambdas, A=5000, sigma=3)
>>> spectrum += HBETA.gaussian_model(lambdas, A=3000, sigma=2)
>>> spectrum += O2_1.gaussian_model(lambdas, A=-3000, sigma=7)
>>> spectrum_err = np.sqrt(spectrum)
>>> spec = Spectrum()
>>> spec.lambdas = lambdas
>>> spec.data = spectrum
>>> spec.err = spectrum_err
>>> fwhm_func = interp1d(lambdas, 0.01 * lambdas)
Detect the lines
>>> lines = Lines([HALPHA, HBETA, O2_1], hydrogen_only=True,
... atmospheric_lines=True, redshift=0, emission_spectrum=True)
>>> global_chisq, res, ws = detect_lines(lines, lambdas, spectrum, spectrum_err, fwhm_func=fwhm_func)
>>> assert(global_chisq < 1.)
Print the result
>>> spec.lines = lines
>>> t = lines.build_detected_line_table()
"""
lambdas = np.zeros(1)
rows = []
j = 0
for line in self.lines:
if not line.use_for_calibration and calibration_only:
continue
if line.fitted is True:
# look for lines in subset fit
bgd_npar = line.fit_bgd_npar
parameters.CALIB_BGD_NPARAMS = bgd_npar
if lambdas.shape != line.fit_lambdas.shape or not np.allclose(lambdas, line.fit_lambdas, 1e-3):
j = 0
lambdas = np.copy(line.fit_lambdas)
popt = line.fit_popt
peak_pos = popt[bgd_npar + 3 * j + 1]
FWHM = np.abs(popt[bgd_npar + 3 * j + 2]) * 2.355
signal_level = popt[bgd_npar + 3 * j]
err = np.sqrt(line.fit_pcov[bgd_npar + 3 * j + 1, bgd_npar + 3 * j + 1])
if line.high_snr:
rows.append((line.label, line.wavelength, peak_pos, peak_pos - line.wavelength, err,
FWHM, signal_level, line.fit_snr, line.fit_chisq, line.fit_eqwidth_mod,
line.fit_eqwidth_data))
j += 1
t = Table()
if len(rows) > 0:
t = Table(rows=rows, names=(
'Line', 'Tabulated', 'Detected', 'Shift', 'Err', 'FWHM', 'Amplitude', 'SNR', 'Chisq', 'Eqwidth_mod',
'Eqwidth_data'),
dtype=('a12', 'f4', 'f4', 'f4', 'f4', 'f4', 'f4', 'f4', 'f4', 'f4', 'f4'))
for col in t.colnames[1:6]:
t[col].unit = 'nm'
t[t.colnames[5]].unit = amplitude_units
for col in t.colnames[-2:]:
t[col].unit = 'nm'
t[t.colnames[-3]].unit = 'reduced'
t.convert_bytestring_to_unicode()
return t
# Line catalog
# Hydrogen lines
HALPHA = Line(656.3, atmospheric=False, label='$H\\alpha$', label_pos=[-0.02, 0.02], use_for_calibration=True)
HBETA = Line(486.3, atmospheric=False, label='$H\\beta$', label_pos=[0.007, 0.02], use_for_calibration=True)
HGAMMA = Line(434.0, atmospheric=False, label='$H\\gamma$', label_pos=[0.007, 0.02], use_for_calibration=True)
HDELTA = Line(410.2, atmospheric=False, label='$H\\delta$', label_pos=[0.007, 0.02], use_for_calibration=True)
HEPSILON = Line(397.0, atmospheric=False, label='$H\\epsilon$', label_pos=[-0.02, 0.02], use_for_calibration=True)
H8 = Line(388.9, atmospheric=False, label='$H8$', label_pos=[-0.02, 0.02], use_for_calibration=True)
H9 = Line(383.5, atmospheric=False, label='$H9$', label_pos=[-0.02, 0.02], use_for_calibration=False)
H10 = Line(379.8, atmospheric=False, label='$H10$', label_pos=[-0.02, 0.02], use_for_calibration=False)
H11 = Line(377.1, atmospheric=False, label='$H11$', label_pos=[-0.02, 0.02], use_for_calibration=False)
H12 = Line(375.0, atmospheric=False, label='$H12$', label_pos=[-0.02, 0.02], use_for_calibration=False)
HYDROGEN_LINES = [HALPHA, HBETA, HGAMMA, HDELTA, HEPSILON, H8, H9, H10, H11, H12]
# Stellar lines (Fraunhofer lines) https://en.wikipedia.org/wiki/Fraunhofer_lines
FE1 = Line(382.044, atmospheric=False, label=r'$Fe_{I}$', label_pos=[-0.016, 0.02])
CAII1 = Line(393.366, atmospheric=False, label=r'$Ca_{II}$', label_pos=[-0.016, 0.02])
CAII2 = Line(396.847, atmospheric=False, label=r'$Ca_{II}$', label_pos=[-0.016, 0.02])
FE2 = Line(430.790, atmospheric=False, label=r'$Fe_{I}$', label_pos=[-0.016, 0.02])
FE3 = Line(438.355, atmospheric=False, label=r'$Fe_{I}$', label_pos=[-0.016, 0.02])
HEI1 = Line(447.1, atmospheric=False, label=r'$He_{I}$', label_pos=[-0.016, 0.02])
MG1 = Line(517.27, atmospheric=False, label=r'$Mg_{I}$', label_pos=[-0.016, 0.02])
MG2 = Line(518.36, atmospheric=False, label=r'$Mg_{I}$', label_pos=[-0.016, 0.02])
FE4 = Line(527.039, atmospheric=False, label=r'$Fe_{I}$', label_pos=[-0.016, 0.02])
STELLAR_LINES = [FE1, FE2, FE3, FE4, CAII1, CAII2, HEI1, MG1, MG2]
# Atmospheric lines
# O2 = Line(762.2, atmospheric=True, label=r'$O_2$', # 762.2 is a weighted average of the O2 line simulated by Libradtran
# label_pos=[0.007, 0.02],
# use_for_calibration=True) # http://onlinelibrary.wiley.com/doi/10.1029/98JD02799/pdf
O2_1 = Line(760.3, atmospheric=True, label='$O_2$',
label_pos=[-0.02, 0.02], use_for_calibration=True) # libradtran paper fig.3
O2_2 = Line(763.1, atmospheric=True, label='$O_2$',
label_pos=[0.007, 0.02], use_for_calibration=True) # libradtran paper fig.3
O2B = Line(687.472, atmospheric=True, label=r'$O_2(B)$', # 687.472 is a weighted average of the O2B line simulated by Libradtran
label_pos=[0.007, 0.02], use_for_calibration=True) # https://en.wikipedia.org/wiki/Fraunhofer_lines
O2Y = Line(898.765, atmospheric=True, label=r'$O_2(Y)$',
label_pos=[0.007, 0.02]) # https://en.wikipedia.org/wiki/Fraunhofer_lines
O2Z = Line(822.696, atmospheric=True, label=r'$O_2(Z)$',
label_pos=[0.007, 0.02]) # https://en.wikipedia.org/wiki/Fraunhofer_lines
# H2O = Line( 960,atmospheric=True,label='$H_2 O$',label_pos=[0.007,0.02],width_bounds=(1,50)) #
H2O_1 = Line(935, atmospheric=True, label=r'$H_2 O$', label_pos=[0.007, 0.02], # MFL: don't these need different labels?
width_bounds=[5, 30]) # libradtran paper fig.3, broad line
# H2O_2 = Line(960, atmospheric=True, label=r'$H_2 O$', label_pos=[0.007, 0.02], # MFL: don't these need different labels?
# width_bounds=[5, 30]) # libradtran paper fig.3, broad line
ATMOSPHERIC_LINES = [O2_1, O2_2, O2B, O2Y, O2Z, H2O_1]
# ISM lines
OIII = Line(500.7, atmospheric=False, label=r'$O_{III}$', label_pos=[0.007, 0.02])
CII1 = Line(723.5, atmospheric=False, label=r'$C_{II}$', label_pos=[0.005, 0.88])
CII2 = Line(711.0, atmospheric=False, label=r'$C_{II}$', label_pos=[0.005, 0.02])
CIV = Line(706.0, atmospheric=False, label=r'$C_{IV}$', label_pos=[-0.016, 0.88])
CII3 = Line(679.0, atmospheric=False, label=r'$C_{II}$', label_pos=[0.005, 0.02])
CIII1 = Line(673.0, atmospheric=False, label=r'$C_{III}$', label_pos=[-0.016, 0.88])
CIII2 = Line(570.0, atmospheric=False, label=r'$C_{III}$', label_pos=[0.007, 0.02])
CIII3 = Line(970.5, atmospheric=False, label=r'$C_{III}$', label_pos=[0.007, 0.02])
FEII1 = Line(463.8, atmospheric=False, label=r'$Fe_{II}$', label_pos=[-0.016, 0.02])
FEII2 = Line(515.8, atmospheric=False, label=r'$Fe_{II}$', label_pos=[0.007, 0.02])
FEII3 = Line(527.3, atmospheric=False, label=r'$Fe_{II}$', label_pos=[0.007, 0.02])
FEII4 = Line(534.9, atmospheric=False, label=r'$Fe_{II}$', label_pos=[0.007, 0.02])
HEI1 = Line(388.8, atmospheric=False, label=r'$He_{I}$', label_pos=[0.007, 0.02])
HEI2 = Line(447.1, atmospheric=False, label=r'$He_{I}$', label_pos=[0.007, 0.02])
HEI3 = Line(587.5, atmospheric=False, label=r'$He_{I}$', label_pos=[0.007, 0.02])
HEI4 = Line(750.0, atmospheric=False, label=r'$He_{I}$', label_pos=[0.007, 0.02])
HEI5 = Line(776.0, atmospheric=False, label=r'$He_{I}$', label_pos=[0.007, 0.02])
HEI6 = Line(781.6, atmospheric=False, label=r'$He_{I}$', label_pos=[0.007, 0.02])
HEI7 = Line(848.2, atmospheric=False, label=r'$He_{I}$', label_pos=[0.007, 0.02])
HEI8 = Line(861.7, atmospheric=False, label=r'$He_{I}$', label_pos=[0.007, 0.02])
HEI9 = Line(906.5, atmospheric=False, label=r'$He_{I}$', label_pos=[0.007, 0.02])
HEI10 = Line(923.5, atmospheric=False, label=r'$He_{I}$', label_pos=[0.007, 0.02])
HEI11 = Line(951.9, atmospheric=False, label=r'$He_{I}$', label_pos=[0.007, 0.02])
HEI12 = Line(1023.5, atmospheric=False, label=r'$He_{I}$', label_pos=[0.007, 0.02])
HEI13 = Line(353.1, atmospheric=False, label=r'$He_{I}$', label_pos=[0.007, 0.02])
OI = Line(630.0, atmospheric=False, label=r'$O_{II}$', label_pos=[0.007, 0.02]) # MFL: label typo?
OII = Line(732.5, atmospheric=False, label=r'$O_{II}$', label_pos=[0.007, 0.02])
HEII1 = Line(468.6, atmospheric=False, label=r'$He_{II}$', label_pos=[0.007, 0.02])
HEII2 = Line(611.8, atmospheric=False, label=r'$He_{II}$', label_pos=[0.007, 0.02])
HEII3 = Line(617.1, atmospheric=False, label=r'$He_{II}$', label_pos=[0.007, 0.02])
HEII4 = Line(856.7, atmospheric=False, label=r'$He_{II}$', label_pos=[0.007, 0.02])
HI = Line(833.9, atmospheric=False, label=r'$H_{I}$', label_pos=[0.007, 0.02])
ISM_LINES = [OIII, CII1, CII2, CIV, CII3, CIII1, CIII2, CIII3, HEI1, HEI2, HEI3, HEI4, HEI5, HEI6, HEI7, HEI8,
HEI9, HEI10, HEI11, HEI12, HEI13, OI, OII, HEII1, HEII2, HEII3, HEII4, HI, FEII1, FEII2, FEII3, FEII4]
# HG-AR lines https://oceanoptics.com/wp-content/uploads/hg1.pdf
HG1 = Line(253.652, atmospheric=False, label=r'$Hg$', label_pos=[0.007, 0.02])
HG2 = Line(296.728, atmospheric=False, label=r'$Hg$', label_pos=[0.007, 0.02])
HG3 = Line(302.150, atmospheric=False, label=r'$Hg$', label_pos=[0.007, 0.02])
HG4 = Line(313.155, atmospheric=False, label=r'$Hg$', label_pos=[0.007, 0.02])
HG5 = Line(334.148, atmospheric=False, label=r'$Hg$', label_pos=[0.007, 0.02])
HG6 = Line(365.015, atmospheric=False, label=r'$Hg$', label_pos=[0.007, 0.02], use_for_calibration=True)
HG7 = Line(404.656, atmospheric=False, label=r'$Hg$', label_pos=[0.007, 0.02], use_for_calibration=True)
HG8 = Line(407.783, atmospheric=False, label=r'$Hg$', label_pos=[0.007, 0.02], use_for_calibration=True)
HG9 = Line(435.833, atmospheric=False, label=r'$Hg$', label_pos=[0.007, 0.02])
HG10 = Line(546.074, atmospheric=False, label=r'$Hg$', label_pos=[0.007, 0.02], use_for_calibration=True)
HG11 = Line(576.960, atmospheric=False, label=r'$Hg$', label_pos=[0.007, 0.02], use_for_calibration=True)
HG12 = Line(579.066, atmospheric=False, label=r'$Hg$', label_pos=[0.007, 0.02], use_for_calibration=True)
AR1 = Line(696.543, atmospheric=False, label=r'$Ar$', label_pos=[0.007, 0.02])
AR2 = Line(706.722, atmospheric=False, label=r'$Ar$', label_pos=[0.007, 0.02])
AR3 = Line(714.704, atmospheric=False, label=r'$Ar$', label_pos=[0.007, 0.02])
AR4 = Line(727.294, atmospheric=False, label=r'$Ar$', label_pos=[0.007, 0.02])
AR5 = Line(738.393, atmospheric=False, label=r'$Ar$', label_pos=[0.007, 0.02])
AR6 = Line(750.387, atmospheric=False, label=r'$Ar$', label_pos=[0.007, 0.02])
AR7 = Line(763.511, atmospheric=False, label=r'$Ar$', label_pos=[0.007, 0.02])
AR8 = Line(772.376, atmospheric=False, label=r'$Ar$', label_pos=[0.007, 0.02])
AR9 = Line(794.818, atmospheric=False, label=r'$Ar$', label_pos=[0.007, 0.02])
AR10 = Line(800.616, atmospheric=False, label=r'$Ar$', label_pos=[0.007, 0.02])
AR11 = Line(811.531, atmospheric=False, label=r'$Ar$', label_pos=[0.007, 0.02])
AR12 = Line(826.452, atmospheric=False, label=r'$Ar$', label_pos=[0.007, 0.02])
AR13 = Line(842.465, atmospheric=False, label=r'$Ar$', label_pos=[0.007, 0.02])
AR14 = Line(852.144, atmospheric=False, label=r'$Ar$', label_pos=[0.007, 0.02])
AR15 = Line(866.794, atmospheric=False, label=r'$Ar$', label_pos=[0.007, 0.02])
AR16 = Line(912.297, atmospheric=False, label=r'$Ar$', label_pos=[0.007, 0.02])
AR17 = Line(922.450, atmospheric=False, label=r'$Ar$', label_pos=[0.007, 0.02])
AR18 = Line(965.7786, atmospheric=False, label=r'$Ar$', label_pos=[0.007, 0.02])
AR19 = Line(978.4503, atmospheric=False, label=r'$Ar$', label_pos=[0.007, 0.02])
HG13 = Line(1013.976, atmospheric=False, label=r'$Hg$', label_pos=[0.007, 0.02])
HGAR_LINES = [HG1, HG2, HG3, HG4, HG5, HG6, HG7, HG8, HG9, HG10, HG11, HG12,
AR1, AR2, AR3, AR4, AR5, AR6, AR7, AR8, AR9, AR10, AR11, AR12, AR13, AR14, AR15, AR16, AR17, AR18, AR19, HG13]
if __name__ == "__main__":
import doctest
doctest.testmod()