@@ -40,30 +40,29 @@ class Ion(IonBase, ContinuumBase):
40
40
input formats.
41
41
temperature : `~astropy.units.Quantity`
42
42
Temperature array over which to evaluate temperature dependent quantities.
43
- ionization_fraction : `str` or `float` or array-like, optional
44
- If a string is provided, use the appropriate "ioneq" dataset. If an array is provided, it must be the same shape
45
- as ``temperature``. If a scalar value is passed in, the ionization fraction is assumed constant at all temperatures.
46
43
abundance : `str` or `float`, optional
47
44
If a string is provided, use the appropriate abundance dataset.
48
45
If a float is provided, use that value as the abundance.
49
- ip_filename : `str`, optional
50
- Ionization potential dataset
46
+ ionization_fraction : `str` or `float` or array-like, optional
47
+ If a string is provided, use the appropriate "ioneq" dataset. If an array is provided, it must be the same shape
48
+ as ``temperature``. If a scalar value is passed in, the ionization fraction is assumed constant at all temperatures.
49
+ ionization_potential : `str` or `~astropy.units.Quantity`, optional
50
+ If a string is provided, use the appropriate "ip" dataset.
51
+ If a scalar value is provided, use that value for the ionization potential. This value should be convertible to eV.
51
52
"""
52
53
53
54
@u .quantity_input
54
55
def __init__ (self , ion_name , temperature : u .K ,
55
- abundance = 'sun_coronal_1992_feldman_ext' ,
56
- ionization_fraction = 'chianti' ,
56
+ abundance = 'sun_coronal_1992_feldman_ext' ,
57
+ ionization_fraction = 'chianti' ,
58
+ ionization_potential = 'chianti' ,
57
59
* args , ** kwargs ):
58
60
super ().__init__ (ion_name , * args , ** kwargs )
59
61
self .temperature = np .atleast_1d (temperature )
60
- # Get selected datasets
61
- # TODO: do not hardcode defaults, pull from rc file
62
62
self ._dset_names = {}
63
- self ._dset_names ['ionization_fraction' ] = kwargs .get ('ionization_fraction' , 'chianti' )
64
- self ._dset_names ['ip_filename' ] = kwargs .get ('ip_filename' , 'chianti' )
65
63
self .abundance = abundance
66
64
self .ionization_fraction = ionization_fraction
65
+ self .ionization_potential = ionization_potential
67
66
self .gaunt_factor = GauntFactor (hdf5_dbase_root = self .hdf5_dbase_root )
68
67
69
68
def _new_instance (self , temperature = None , ** kwargs ):
@@ -101,8 +100,8 @@ def __repr__(self):
101
100
HDF5 Database: { self .hdf5_dbase_root }
102
101
Using Datasets:
103
102
ionization_fraction: { self ._dset_names ['ionization_fraction' ]}
104
- abundance: { self ._dset_names . get ( 'abundance' , self . abundance ) }
105
- ip: { self ._dset_names ['ip_filename ' ]} """
103
+ abundance: { self ._dset_names [ 'abundance' ] }
104
+ ip: { self ._dset_names ['ionization_potential ' ]} """
106
105
107
106
@cached_property
108
107
def _all_levels (self ):
@@ -141,16 +140,16 @@ def _instance_kwargs(self):
141
140
'hdf5_dbase_root' : self .hdf5_dbase_root ,
142
141
** self ._dset_names ,
143
142
}
144
- # If the abundance is set using a string specifying the abundance dataset,
143
+ # If any of the datasets are set using a string specifying the name of the dataset,
145
144
# the dataset name is in _dset_names. We want to pass this to the new instance
146
- # so that the new instance knows that the abundance was specified using a
147
- # dataset name. Otherwise, we can just pass the actual abundance value.
145
+ # so that the new instance knows that the dataset was specified using a
146
+ # dataset name. Otherwise, we can just pass the actual value.
148
147
if kwargs ['abundance' ] is None :
149
148
kwargs ['abundance' ] = self .abundance
150
-
151
149
if kwargs ['ionization_fraction' ] is None :
152
150
kwargs ['ionization_fraction' ] = self .ionization_fraction
153
-
151
+ if kwargs ['ionization_potential' ] is None :
152
+ kwargs ['ionization_potential' ] = self .ionization_potential
154
153
return kwargs
155
154
156
155
def _has_dataset (self , dset_name ):
@@ -193,29 +192,42 @@ def transitions(self):
193
192
return Transitions (self ._elvlc , self ._wgfa )
194
193
195
194
@property
196
- def ionization_fraction (self ):
195
+ @u .quantity_input
196
+ def ionization_fraction (self ) -> u .dimensionless_unscaled :
197
197
"""
198
198
Ionization fraction of an ion
199
199
"""
200
+ if self ._ionization_fraction is None :
201
+ raise MissingDatasetException (
202
+ f"{ self ._dset_names ['ionization_fraction' ]} ionization fraction data missing for { self .ion_name } "
203
+ )
200
204
return self ._ionization_fraction
201
205
202
206
@ionization_fraction .setter
203
207
def ionization_fraction (self , ionization_fraction ):
204
208
if isinstance (ionization_fraction , str ):
205
209
self ._dset_names ['ionization_fraction' ] = ionization_fraction
206
- self ._ionization_fraction = self ._interpolate_ionization_fraction ()
210
+ ionization_fraction = None
211
+ if self ._has_dataset ('ion_fraction' ) and (ionization_fraction := self ._ion_fraction .get (self ._dset_names ['ionization_fraction' ])):
212
+ ionization_fraction = self ._interpolate_ionization_fraction (
213
+ self .temperature ,
214
+ ionization_fraction ['temperature' ],
215
+ ionization_fraction ['ionization_fraction' ]
216
+ )
217
+ self ._ionization_fraction = ionization_fraction
207
218
else :
208
219
# Multiplying by np.ones allows for passing in scalar values
209
- _ionization_fraction = np .atleast_1d (ionization_fraction ) * np .ones (self .temperature .shape )
220
+ ionization_fraction = np .atleast_1d (ionization_fraction ) * np .ones (self .temperature .shape )
210
221
self ._dset_names ['ionization_fraction' ] = None
211
- self ._ionization_fraction = _ionization_fraction
222
+ self ._ionization_fraction = ionization_fraction
212
223
213
- def _interpolate_ionization_fraction (self ):
224
+ @staticmethod
225
+ def _interpolate_ionization_fraction (temperature , temperature_data , ionization_data ):
214
226
"""
215
227
Ionization equilibrium data interpolated to the given temperature
216
228
217
- Interpolated the pre-computed ionization fractions stored in CHIANTI to the temperature
218
- of the ion . Returns NaN where interpolation is out of range of the data. For computing
229
+ Interpolated the pre-computed ionization fractions stored in CHIANTI to a new temperature
230
+ array . Returns NaN where interpolation is out of range of the data. For computing
219
231
ionization equilibrium outside of this temperature range, it is better to use the ionization
220
232
and recombination rates.
221
233
@@ -225,13 +237,22 @@ def _interpolate_ionization_fraction(self):
225
237
Interpolating Polynomial with `~scipy.interpolate.PchipInterpolator`. This helps to
226
238
ensure smoothness while reducing oscillations in the interpolated ionization fractions.
227
239
240
+ Parameters
241
+ ----------
242
+ temperature: `~astropy.units.Quantity`
243
+ Temperature array to interpolation onto.
244
+ temperature_data: `~astropy.units.Quantity`
245
+ Temperature array on which the ionization fraction is defined
246
+ ionization_data: `~astropy.units.Quantity`
247
+ Ionization fraction as a function of temperature.
248
+
228
249
See Also
229
250
--------
230
251
fiasco.Element.equilibrium_ionization
231
252
"""
232
- temperature = self . temperature .to_value ('K' )
233
- temperature_data = self . _ion_fraction [ self . _dset_names [ 'ionization_fraction' ]][ 'temperature' ] .to_value ('K' )
234
- ionization_data = self . _ion_fraction [ self . _dset_names [ 'ionization_fraction' ]][ 'ionization_fraction' ]. value
253
+ temperature = temperature .to_value ('K' )
254
+ temperature_data = temperature_data .to_value ('K' )
255
+ ionization_data = ionization_data . to_value ()
235
256
# Perform PCHIP interpolation in log-space on only the non-zero ionization fractions.
236
257
# See https://github.com/wtbarnes/fiasco/pull/223 for additional discussion.
237
258
is_nonzero = ionization_data > 0.0
@@ -255,6 +276,10 @@ def abundance(self) -> u.dimensionless_unscaled:
255
276
"""
256
277
Elemental abundance relative to H.
257
278
"""
279
+ if self ._abundance is None :
280
+ raise MissingDatasetException (
281
+ f"{ self ._dset_names ['abundance' ]} abundance data missing for { self .ion_name } "
282
+ )
258
283
return self ._abundance
259
284
260
285
@abundance .setter
@@ -264,21 +289,43 @@ def abundance(self, abundance):
264
289
If the abundance is given as a string, use the matching abundance set.
265
290
If the abundance is given as a float, use that value directly.
266
291
"""
292
+ self ._dset_names ['abundance' ] = None
267
293
if isinstance (abundance , str ):
268
294
self ._dset_names ['abundance' ] = abundance
269
- self . _abundance = self . _abund [ self . _dset_names [ 'abundance' ]]
270
- else :
271
- self ._dset_names ['abundance' ] = None
272
- self ._abundance = abundance
295
+ abundance = None
296
+ if self . _has_dataset ( 'abund' ) :
297
+ abundance = self ._abund . get ( self . _dset_names ['abundance' ])
298
+ self ._abundance = abundance
273
299
274
300
@property
275
- @needs_dataset ('ip' )
276
301
@u .quantity_input
277
- def ip (self ) -> u .erg :
302
+ def ionization_potential (self ) -> u .eV :
278
303
"""
279
304
Ionization potential.
280
305
"""
281
- return self ._ip [self ._dset_names ['ip_filename' ]] * const .h * const .c
306
+ if self ._ionization_potential is None :
307
+ raise MissingDatasetException (
308
+ f"{ self ._dset_names ['ionization_potential' ]} ionization potential data missing for { self .ion_name } "
309
+ )
310
+ # NOTE: Ionization potentials in CHIANTI are stored in units of cm^-1
311
+ # Using this here also means that ionization potentials can be passed
312
+ # in wavenumber units as well.
313
+ return self ._ionization_potential .to ('eV' , equivalencies = u .spectral ())
314
+
315
+ @ionization_potential .setter
316
+ def ionization_potential (self , ionization_potential ):
317
+ """
318
+ Sets the ionization potential of an ion.
319
+ If the ionization potential is given as a string, use the matching ionization potential set.
320
+ if the ionization potential is given as a float, use that value directly.
321
+ """
322
+ self ._dset_names ['ionization_potential' ] = None
323
+ if isinstance (ionization_potential , str ):
324
+ self ._dset_names ['ionization_potential' ] = ionization_potential
325
+ ionization_potential = None
326
+ if self ._has_dataset ('ip' ):
327
+ ionization_potential = self ._ip .get (self ._dset_names ['ionization_potential' ])
328
+ self ._ionization_potential = ionization_potential
282
329
283
330
@property
284
331
def hydrogenic (self ):
@@ -718,7 +765,7 @@ def _population_correction(self, population, density, rate_matrix):
718
765
ratio [:, 0 ] = 0.0
719
766
return 1.0 + ratio
720
767
721
- @needs_dataset ('abundance' , ' elvlc' )
768
+ @needs_dataset ('elvlc' )
722
769
@u .quantity_input
723
770
def contribution_function (self , density : u .cm ** (- 3 ), ** kwargs ) -> u .cm ** 3 * u .erg / u .s :
724
771
r"""
@@ -921,7 +968,6 @@ def spectrum(self, *args, **kwargs):
921
968
return IonCollection (self ).spectrum (* args , ** kwargs )
922
969
923
970
@cached_property
924
- @needs_dataset ('ip' )
925
971
@u .quantity_input
926
972
def direct_ionization_rate (self ) -> u .cm ** 3 / u .s :
927
973
r"""
@@ -961,11 +1007,11 @@ def direct_ionization_rate(self) -> u.cm**3 / u.s:
961
1007
"""
962
1008
xgl , wgl = np .polynomial .laguerre .laggauss (12 )
963
1009
kBT = const .k_B * self .temperature
964
- energy = np .outer (xgl , kBT ) + self .ip
1010
+ energy = np .outer (xgl , kBT ) + self .ionization_potential
965
1011
cross_section = self .direct_ionization_cross_section (energy )
966
- term1 = np .sqrt (8. / np .pi / const .m_e )* np .sqrt (kBT )* np .exp (- self .ip / kBT )
1012
+ term1 = np .sqrt (8. / np .pi / const .m_e )* np .sqrt (kBT )* np .exp (- self .ionization_potential / kBT )
967
1013
term2 = ((wgl * xgl )[:, np .newaxis ]* cross_section ).sum (axis = 0 )
968
- term3 = (wgl [:, np .newaxis ]* cross_section ).sum (axis = 0 )* self .ip / kBT
1014
+ term3 = (wgl [:, np .newaxis ]* cross_section ).sum (axis = 0 )* self .ionization_potential / kBT
969
1015
return term1 * (term2 + term3 )
970
1016
971
1017
@u .quantity_input
@@ -1038,10 +1084,9 @@ def _dere_cross_section(self, energy: u.erg) -> u.cm**2:
1038
1084
1039
1085
return cross_section_total
1040
1086
1041
- @needs_dataset ('ip' )
1042
1087
@u .quantity_input
1043
1088
def _fontes_cross_section (self , energy : u .erg ) -> u .cm ** 2 :
1044
- U = energy / self .ip
1089
+ U = energy / self .ionization_potential
1045
1090
A = 1.13
1046
1091
B = 1 if self .hydrogenic else 2
1047
1092
F = 1 if self .atomic_number < 20 else (140 + (self .atomic_number / 20 )** 3.2 )/ 141
@@ -1057,7 +1102,7 @@ def _fontes_cross_section(self, energy: u.erg) -> u.cm**2:
1057
1102
1058
1103
# NOTE: conversion to Rydbergs equivalent to scaling to the ionization energy
1059
1104
# of hydrogen such that it is effectively unitless
1060
- return B * (np .pi * const .a0 ** 2 ) * F * Qrp / (self .ip . to (u .Ry ). value ** 2 )
1105
+ return B * (np .pi * const .a0 ** 2 ) * F * Qrp / (self .ionization_potential . to_value (u .Ry )** 2 )
1061
1106
1062
1107
@cached_property
1063
1108
@needs_dataset ('easplups' )
@@ -1440,7 +1485,7 @@ def free_bound(self,
1440
1485
self ._fblvl ['L' ],
1441
1486
level_fb ):
1442
1487
# Energy required to ionize ion from level i
1443
- E_ionize = self .ip - E
1488
+ E_ionize = self .ionization_potential - E
1444
1489
# Check if ionization potential and photon energy sufficient
1445
1490
if (E_ionize < 0 * u .erg ) or (E_photon .max () < E ):
1446
1491
continue
@@ -1528,19 +1573,19 @@ def free_bound_radiative_loss(self) -> u.erg * u.cm**3 / u.s:
1528
1573
E_th = recombined ._fblvl ['E_th' ]* const .h * const .c
1529
1574
n0 = recombined ._fblvl ['n' ][0 ]
1530
1575
E_fb = np .where (E_obs == 0 * u .erg , E_th , E_obs )
1531
- wvl_n0 = const . h * const . c / (recombined .ip - E_fb [0 ])
1576
+ wvl_n0 = 1 / (recombined .ionization_potential - E_fb [0 ]). to ( 'cm-1' , equivalencies = u . spectral () )
1532
1577
wvl_n1 = (n0 + 1 )** 2 / (const .Ryd * self .charge_state ** 2 )
1533
1578
g_fb0 = self .gaunt_factor .free_bound_integrated (self .temperature ,
1534
1579
self .atomic_number ,
1535
1580
self .charge_state ,
1536
1581
n0 ,
1537
- recombined .ip ,
1582
+ recombined .ionization_potential ,
1538
1583
ground_state = True )
1539
1584
g_fb1 = self .gaunt_factor .free_bound_integrated (self .temperature ,
1540
1585
self .atomic_number ,
1541
1586
self .charge_state ,
1542
1587
n0 ,
1543
- recombined .ip ,
1588
+ recombined .ionization_potential ,
1544
1589
ground_state = False )
1545
1590
term1 = g_fb0 * np .exp (- const .h * const .c / (const .k_B * self .temperature * wvl_n0 ))
1546
1591
term2 = g_fb1 * np .exp (- const .h * const .c / (const .k_B * self .temperature * wvl_n1 ))
0 commit comments