Skip to content

Commit 63a2778

Browse files
authored
Add thermal_energy property and consolidate base classes (#343)
* consolidate base classes to IonBase * add thermal energy property * update paper refs
1 parent baca1bb commit 63a2778

File tree

4 files changed

+81
-58
lines changed

4 files changed

+81
-58
lines changed

docs/reference/index.rst

+2
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ Each version of the CHIANTI database is described in detail in a set of publicat
3434
- Version 8: :cite:t:`del_zanna_chianti_2015`
3535
- Version 9: :cite:t:`dere_chiantiatomic_2019`
3636
- Version 10: :cite:t:`del_zanna_chiantiatomic_2021`
37+
- Version 10.1: :cite:t:`dere_chianti-atomic_2023`
38+
- Version 11: :cite:t:`dufresne_chiantiatomic_2024`
3739

3840
Bibliography
3941
------------

docs/references.bib

+31
Original file line numberDiff line numberDiff line change
@@ -500,3 +500,34 @@ @techreport{young_chianti_2021
500500
langid = {english},
501501
institution = {None}
502502
}
503+
504+
@article{dere_chianti-atomic_2023,
505+
title = {{{CHIANTI-An Atomic Database}} for {{Emission Lines}}. {{XVII}}. {{Version}} 10.1: {{Revised Ionization}} and {{Recombination Rates}} and {{Other Updates}}},
506+
shorttitle = {{{CHIANTI-An Atomic Database}} for {{Emission Lines}}. {{XVII}}. {{Version}} 10.1},
507+
author = {Dere, Kenneth P. and Del Zanna, G. and Young, P. R. and Landi, E.},
508+
year = {2023},
509+
month = oct,
510+
journal = {The Astrophysical Journal Supplement Series},
511+
volume = {268},
512+
pages = {52},
513+
issn = {0067-0049},
514+
doi = {10.3847/1538-4365/acec79},
515+
urldate = {2024-04-08},
516+
annotation = {ADS Bibcode: 2023ApJS..268...52D}
517+
}
518+
519+
@article{dufresne_chiantiatomic_2024,
520+
title = {{{CHIANTI}}---{{An Atomic Database}} for {{Emission Lines}}---{{Paper}}. {{XVIII}}. {{Version}} 11, {{Advanced Ionization Equilibrium Models}}: {{Density}} and {{Charge Transfer Effects}}},
521+
shorttitle = {{{CHIANTI}}---{{An Atomic Database}} for {{Emission Lines}}---{{Paper}}. {{XVIII}}. {{Version}} 11, {{Advanced Ionization Equilibrium Models}}},
522+
author = {Dufresne, R. P. and Del Zanna, G. and Young, P. R. and Dere, K. P. and Deliporanidou, E. and Barnes, W. T. and Landi, E.},
523+
year = {2024},
524+
month = oct,
525+
journal = {The Astrophysical Journal},
526+
volume = {974},
527+
pages = {71},
528+
publisher = {IOP},
529+
issn = {0004-637X},
530+
doi = {10.3847/1538-4357/ad6765},
531+
urldate = {2025-01-28},
532+
annotation = {ADS Bibcode: 2024ApJ...974...71D}
533+
}

fiasco/base.py

+6-27
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,15 @@
1515
from fiasco.util import check_database, parse_ion_name
1616
from fiasco.util.exceptions import MissingIonError
1717

18-
__all__ = ['Base', 'IonBase', 'ContinuumBase']
18+
__all__ = ['IonBase']
1919

2020

21-
class Base:
21+
class IonBase:
2222
"""
23-
Base class for setting up ion metadata and building database if necessary.
23+
Base class for accessing data attached to a particular ion.
24+
25+
.. note:: This is not meant to be instantiated directly by the user
26+
and primarily serves as a base class for `~fiasco.Ion`.
2427
2528
Parameters
2629
----------
@@ -97,14 +100,6 @@ def ion_name_roman(self):
97100
"Name of the element and ionization stage in roman numeral format."
98101
return f'{self.atomic_symbol} {self.ionization_stage_roman}'
99102

100-
101-
class ContinuumBase(Base):
102-
"""
103-
Base class for retrieving continuum datasets.
104-
105-
.. note:: This is not meant to be instantiated directly by the user
106-
and primarily serves as a base class for `~fiasco.Ion`.
107-
"""
108103
@property
109104
def _verner(self):
110105
data_path = '/'.join([self.atomic_symbol.lower(), self._ion_name, 'continuum',
@@ -121,22 +116,6 @@ def _heseq(self):
121116
data_path = '/'.join([self.atomic_symbol.lower(), 'continuum', 'heseq_2photon'])
122117
return DataIndexer.create_indexer(self.hdf5_dbase_root, data_path)
123118

124-
125-
class IonBase(Base):
126-
"""
127-
Base class for accessing CHIANTI data attached to a particular ion
128-
129-
.. note::
130-
This is not meant to be instantiated directly by the user
131-
and primarily serves as a base class for `~fiasco.Ion`.
132-
133-
Parameters
134-
----------
135-
ion_name : `str`
136-
Name of ion, e.g. for Fe V, 'Fe 5', 'iron 5', 'Fe 4+'
137-
hdf5_path : `str`, optional
138-
"""
139-
140119
@property
141120
def _abund(self):
142121
data_path = '/'.join([self.atomic_symbol.lower(), 'abundance'])

fiasco/ions.py

+42-31
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from scipy.interpolate import CubicSpline, interp1d, PchipInterpolator
1010

1111
from fiasco import proton_electron_ratio
12-
from fiasco.base import ContinuumBase, IonBase
12+
from fiasco.base import IonBase
1313
from fiasco.collections import IonCollection
1414
from fiasco.gaunt import GauntFactor
1515
from fiasco.levels import Level, Transitions
@@ -24,7 +24,7 @@
2424
__all__ = ['Ion']
2525

2626

27-
class Ion(IonBase, ContinuumBase):
27+
class Ion(IonBase):
2828
"""
2929
Class for representing a CHIANTI ion.
3030
@@ -52,11 +52,14 @@ class Ion(IonBase, ContinuumBase):
5252
"""
5353

5454
@u.quantity_input
55-
def __init__(self, ion_name, temperature: u.K,
56-
abundance='sun_coronal_1992_feldman_ext',
57-
ionization_fraction='chianti',
58-
ionization_potential='chianti',
59-
*args, **kwargs):
55+
def __init__(self,
56+
ion_name,
57+
temperature: u.K,
58+
abundance='sun_coronal_1992_feldman_ext',
59+
ionization_fraction='chianti',
60+
ionization_potential='chianti',
61+
*args,
62+
**kwargs):
6063
super().__init__(ion_name, *args, **kwargs)
6164
self.temperature = np.atleast_1d(temperature)
6265
self._dset_names = {}
@@ -163,6 +166,14 @@ def _has_dataset(self, dset_name):
163166
else:
164167
return True
165168

169+
@property
170+
@u.quantity_input
171+
def thermal_energy(self) -> u.erg:
172+
"""
173+
Thermal energy, :math:`k_BT`, as a function of temperature.
174+
"""
175+
return self.temperature.to('erg', equivalencies=u.equivalencies.temperature_energy())
176+
166177
def next_ion(self):
167178
"""
168179
Return an `~fiasco.Ion` instance with the next highest ionization stage.
@@ -373,7 +384,7 @@ def effective_collision_strength(self) -> u.dimensionless_unscaled:
373384
--------
374385
fiasco.util.burgess_tully_descale : Descale and interpolate :math:`\Upsilon`.
375386
"""
376-
kBTE = np.outer(const.k_B * self.temperature, 1.0 / self._scups['delta_energy'])
387+
kBTE = np.outer(self.thermal_energy, 1.0 / self._scups['delta_energy'])
377388
upsilon = burgess_tully_descale(self._scups['bt_t'],
378389
self._scups['bt_upsilon'],
379390
kBTE.T,
@@ -406,10 +417,10 @@ def electron_collision_deexcitation_rate(self) -> u.cm**3 / u.s:
406417
electron_collision_excitation_rate : Excitation rate due to collisions
407418
effective_collision_strength : Maxwellian-averaged collision strength, :math:`\Upsilon`
408419
"""
409-
c = (const.h**2) / ((2. * np.pi * const.m_e)**(1.5) * np.sqrt(const.k_B))
420+
c = const.h**2 / (2. * np.pi * const.m_e)**(1.5)
410421
upsilon = self.effective_collision_strength
411422
omega_upper = 2. * self._elvlc['J'][self._scups['upper_level'] - 1] + 1.
412-
return c * upsilon / np.sqrt(self.temperature[:, np.newaxis]) / omega_upper
423+
return c * upsilon / np.sqrt(self.thermal_energy[:, np.newaxis]) / omega_upper
413424

414425
@cached_property
415426
@needs_dataset('elvlc', 'scups')
@@ -439,7 +450,7 @@ def electron_collision_excitation_rate(self) -> u.cm**3 / u.s:
439450
"""
440451
omega_upper = 2. * self._elvlc['J'][self._scups['upper_level'] - 1] + 1.
441452
omega_lower = 2. * self._elvlc['J'][self._scups['lower_level'] - 1] + 1.
442-
kBTE = np.outer(1./const.k_B/self.temperature, self._scups['delta_energy'])
453+
kBTE = np.outer(1./self.thermal_energy, self._scups['delta_energy'])
443454
return omega_upper / omega_lower * self.electron_collision_deexcitation_rate * np.exp(-kBTE)
444455

445456
@cached_property
@@ -461,7 +472,7 @@ def proton_collision_excitation_rate(self) -> u.cm**3 / u.s:
461472
# Create scaled temperature--these are not stored in the file
462473
bt_t = [np.linspace(0, 1, ups.shape[0]) for ups in self._psplups['bt_rate']]
463474
# Get excitation rates directly from scaled data
464-
kBTE = np.outer(const.k_B * self.temperature, 1.0 / self._psplups['delta_energy'])
475+
kBTE = np.outer(self.thermal_energy, 1.0 / self._psplups['delta_energy'])
465476
ex_rate = burgess_tully_descale(bt_t,
466477
self._psplups['bt_rate'],
467478
kBTE.T,
@@ -494,7 +505,7 @@ def proton_collision_deexcitation_rate(self) -> u.cm**3 / u.s:
494505
--------
495506
proton_collision_excitation_rate : Excitation rate due to collisions with protons
496507
"""
497-
kBTE = np.outer(const.k_B * self.temperature, 1.0 / self._psplups['delta_energy'])
508+
kBTE = np.outer(self.thermal_energy, 1.0 / self._psplups['delta_energy'])
498509
omega_upper = 2. * self._elvlc['J'][self._psplups['upper_level'] - 1] + 1.
499510
omega_lower = 2. * self._elvlc['J'][self._psplups['lower_level'] - 1] + 1.
500511
dex_rate = (omega_lower / omega_upper) * self.proton_collision_excitation_rate * np.exp(1. / kBTE)
@@ -1006,7 +1017,7 @@ def direct_ionization_rate(self) -> u.cm**3 / u.s:
10061017
direct_ionization_cross_section : Calculation of :math:`\sigma_I` as a function of :math:`E`.
10071018
"""
10081019
xgl, wgl = np.polynomial.laguerre.laggauss(12)
1009-
kBT = const.k_B * self.temperature
1020+
kBT = self.thermal_energy
10101021
energy = np.outer(xgl, kBT) + self.ionization_potential
10111022
cross_section = self.direct_ionization_cross_section(energy)
10121023
term1 = np.sqrt(8./np.pi/const.m_e)*np.sqrt(kBT)*np.exp(-self.ionization_potential/kBT)
@@ -1126,8 +1137,8 @@ def excitation_autoionization_rate(self) -> u.cm**3 / u.s:
11261137
Additionally, note that the constant has been rewritten in terms of :math:`h`
11271138
rather than :math:`I_H` and :math:`a_0`.
11281139
"""
1129-
c = (const.h**2)/((2. * np.pi * const.m_e)**(1.5) * np.sqrt(const.k_B))
1130-
kBTE = np.outer(const.k_B*self.temperature, 1.0/self._easplups['delta_energy'])
1140+
c = const.h**2/(2. * np.pi * const.m_e)**(1.5)
1141+
kBTE = np.outer(self.thermal_energy, 1.0/self._easplups['delta_energy'])
11311142
# NOTE: Transpose here to make final dimensions compatible with multiplication with
11321143
# temperature when computing rate
11331144
kBTE = kBTE.T
@@ -1139,7 +1150,7 @@ def excitation_autoionization_rate(self) -> u.cm**3 / u.s:
11391150
self._easplups['bt_type'])
11401151
# NOTE: The 1/omega multiplicity factor is already included in the scaled upsilon
11411152
# values provided by CHIANTI
1142-
rate = c * upsilon * np.exp(-1 / kBTE) / np.sqrt(self.temperature)
1153+
rate = c * upsilon * np.exp(-1 / kBTE) / np.sqrt(self.thermal_energy)
11431154

11441155
return rate.sum(axis=0)
11451156

@@ -1369,13 +1380,13 @@ def free_free(self, wavelength: u.angstrom) -> u.erg * u.cm**3 / u.s / u.angstro
13691380
fiasco.IonCollection.free_free: Includes abundance and ionization equilibrium.
13701381
"""
13711382
prefactor = (const.c / 3. / const.m_e * (const.alpha * const.h / np.pi)**3
1372-
* np.sqrt(2. * np.pi / 3. / const.m_e / const.k_B))
1373-
tmp = np.outer(self.temperature, wavelength)
1374-
exp_factor = np.exp(-const.h * const.c / const.k_B / tmp) / (wavelength**2)
1383+
* np.sqrt(2. * np.pi / 3. / const.m_e))
1384+
tmp = np.outer(self.thermal_energy, wavelength)
1385+
exp_factor = np.exp(-const.h * const.c / tmp) / (wavelength**2)
13751386
gf = self.gaunt_factor.free_free(self.temperature, wavelength, self.atomic_number, self.charge_state, )
13761387

13771388
return (prefactor * self.charge_state**2 * exp_factor * gf
1378-
/ np.sqrt(self.temperature)[:, np.newaxis])
1389+
/ np.sqrt(self.thermal_energy)[:, np.newaxis])
13791390

13801391
@u.quantity_input
13811392
def free_free_radiative_loss(self, use_itoh=False) -> u.erg * u.cm**3 / u.s:
@@ -1414,9 +1425,9 @@ def free_free_radiative_loss(self, use_itoh=False) -> u.erg * u.cm**3 / u.s:
14141425
--------
14151426
fiasco.GauntFactor.free_free_integrated: Calculation of :math:`\langle g_{t,ff}\rangle`.
14161427
"""
1417-
prefactor = (16./3**1.5) * np.sqrt(2. * np.pi * const.k_B/(const.hbar**2 * const.m_e**3)) * (const.e.esu**6 / const.c**3)
1428+
prefactor = (16./3**1.5) * np.sqrt(2. * np.pi / (const.hbar**2 * const.m_e**3)) * (const.e.esu**6 / const.c**3)
14181429
gf = self.gaunt_factor.free_free_integrated(self.temperature, self.charge_state, use_itoh=use_itoh)
1419-
return (prefactor * self.charge_state**2 * gf * np.sqrt(self.temperature))
1430+
return (prefactor * self.charge_state**2 * gf * np.sqrt(self.thermal_energy))
14201431

14211432
@needs_dataset('fblvl', 'ip')
14221433
@u.quantity_input
@@ -1465,12 +1476,12 @@ def free_bound(self,
14651476
:cite:t:`verner_analytic_1995`.
14661477
"""
14671478
wavelength = np.atleast_1d(wavelength)
1468-
prefactor = (2/np.sqrt(2*np.pi)/(const.h*(const.c**3) * (const.m_e * const.k_B)**(3/2)))
1479+
prefactor = 2/np.sqrt(2*np.pi)/(const.h*(const.c**3) * const.m_e**(3/2))
14691480
recombining = self.next_ion()
14701481
omega_0 = recombining._fblvl['multiplicity'][0] if recombining._has_dataset('fblvl') else 1.0
14711482
E_photon = const.h * const.c / wavelength
14721483
# Precompute this here to avoid repeated outer product calculations
1473-
exp_energy_ratio = np.exp(-np.outer(1/(const.k_B*self.temperature), E_photon))
1484+
exp_energy_ratio = np.exp(-np.outer(1/self.thermal_energy, E_photon))
14741485
# Fill in observed energies with theoretical energies
14751486
E_obs = self._fblvl['E_obs']*const.h*const.c
14761487
E_th = self._fblvl['E_th']*const.h*const.c
@@ -1499,13 +1510,13 @@ def free_bound(self,
14991510
# At these temperatures, the cross-section is 0 anyway so we can just zero
15001511
# these terms. Just multiplying by 0 is not sufficient because 0*inf=inf
15011512
with np.errstate(over='ignore', invalid='ignore'):
1502-
exp_ip_ratio = np.exp(E_ionize/(const.k_B*self.temperature))
1513+
exp_ip_ratio = np.exp(E_ionize/self.thermal_energy)
15031514
xs_exp_ip_ratio = np.outer(exp_ip_ratio, cross_section)
15041515
xs_exp_ip_ratio[:,cross_section==0.0*u.cm**2] = 0.0 * u.cm**2
15051516
sum_factor += omega * xs_exp_ip_ratio
15061517

15071518
return (prefactor
1508-
* np.outer(self.temperature**(-3/2), E_photon**5)
1519+
* np.outer(self.thermal_energy**(-3/2), E_photon**5)
15091520
* exp_energy_ratio
15101521
* sum_factor / omega_0)
15111522

@@ -1566,8 +1577,8 @@ def free_bound_radiative_loss(self) -> u.erg * u.cm**3 / u.s:
15661577
recombined = self.previous_ion()
15671578
if not recombined._has_dataset('fblvl'):
15681579
return u.Quantity(np.zeros(self.temperature.shape) * u.erg * u.cm**3 / u.s)
1569-
C_ff = 64 * np.pi / 3.0 * np.sqrt(np.pi/6.) * (const.e.esu**6)/(const.c**2 * const.m_e**1.5 * const.k_B**0.5)
1570-
prefactor = C_ff * const.k_B * np.sqrt(self.temperature) / (const.h*const.c)
1580+
C_ff = 64 * np.pi / 3.0 * np.sqrt(np.pi/6.) * (const.e.esu**6)/(const.c**2 * const.m_e**1.5)
1581+
prefactor = C_ff * np.sqrt(self.thermal_energy) / (const.h*const.c)
15711582

15721583
E_obs = recombined._fblvl['E_obs']*const.h*const.c
15731584
E_th = recombined._fblvl['E_th']*const.h*const.c
@@ -1587,8 +1598,8 @@ def free_bound_radiative_loss(self) -> u.erg * u.cm**3 / u.s:
15871598
n0,
15881599
recombined.ionization_potential,
15891600
ground_state=False)
1590-
term1 = g_fb0 * np.exp(-const.h*const.c/(const.k_B * self.temperature * wvl_n0))
1591-
term2 = g_fb1 * np.exp(-const.h*const.c/(const.k_B * self.temperature * wvl_n1))
1601+
term1 = g_fb0 * np.exp(-const.h*const.c/(self.thermal_energy * wvl_n0))
1602+
term2 = g_fb1 * np.exp(-const.h*const.c/(self.thermal_energy * wvl_n1))
15921603

15931604
return prefactor * (term1 + term2)
15941605

0 commit comments

Comments
 (0)