From 08846634f530d04dda7a713433851690d9dcc416 Mon Sep 17 00:00:00 2001 From: jacobdparker Date: Fri, 18 Sep 2026 15:41:53 -0600 Subject: [PATCH 01/10] Use the cached proton-electron ratio in `Ion.emissivity` and make it settable `Ion.emissivity` called the module-level `proton_electron_ratio` directly, walking the entire database on every call, instead of using the cached property on the instance. Replace the `cached_property` with a lazily computed property that has a setter, so a ratio computed once with `fiasco.proton_electron_ratio` can be shared between ions with the same temperature. Fixes #470 Co-Authored-By: Claude Fable 5.1 --- changelog/471.bugfix.rst | 1 + fiasco/ions.py | 24 ++++++++++++++++++++---- fiasco/tests/test_ion.py | 19 +++++++++++++++++++ 3 files changed, 40 insertions(+), 4 deletions(-) create mode 100644 changelog/471.bugfix.rst diff --git a/changelog/471.bugfix.rst b/changelog/471.bugfix.rst new file mode 100644 index 00000000..504858b8 --- /dev/null +++ b/changelog/471.bugfix.rst @@ -0,0 +1 @@ +`fiasco.Ion.emissivity` now uses the cached `fiasco.Ion.proton_electron_ratio` property rather than recomputing the proton-to-electron ratio, which walks the entire database, on every call. `fiasco.Ion.proton_electron_ratio` can now also be set directly so that a ratio computed once with `fiasco.proton_electron_ratio` can be shared between ions with the same temperature. diff --git a/fiasco/ions.py b/fiasco/ions.py index dceede9f..266820ff 100644 --- a/fiasco/ions.py +++ b/fiasco/ions.py @@ -70,6 +70,7 @@ def __init__(self, super().__init__(ion_name, *args, **kwargs) self.temperature = np.atleast_1d(temperature) self._dset_names = {} + self._proton_electron_ratio = None self.abundance = abundance self.ionization_fraction = ionization_fraction self.ionization_potential = ionization_potential @@ -203,10 +204,26 @@ def thermal_energy(self) -> u.erg: """ return self.temperature.to('erg', equivalencies=u.equivalencies.temperature_energy()) - @cached_property + @property @u.quantity_input def proton_electron_ratio(self) -> u.dimensionless_unscaled: - return proton_electron_ratio(self.temperature, **self._instance_kwargs) + """ + Ratio of proton to electron number density as a function of temperature. + + Computed with `fiasco.proton_electron_ratio` on first access and cached afterward. + The ratio depends only on the temperature and the abundance and ionization fraction + datasets, so it can also be set directly to avoid recomputing it for every ion that + shares the same temperature, e.g. + ``ion.proton_electron_ratio = fiasco.proton_electron_ratio(ion.temperature)``. + """ + if self._proton_electron_ratio is None: + self._proton_electron_ratio = proton_electron_ratio(self.temperature, **self._instance_kwargs) + return self._proton_electron_ratio + + @proton_electron_ratio.setter + def proton_electron_ratio(self, value): + # Multiplying by np.ones allows for passing in scalar values + self._proton_electron_ratio = np.atleast_1d(value) * np.ones(self.temperature.shape) def next_ion(self): """ @@ -1214,8 +1231,7 @@ def emissivity(self, density: u.cm**(-3), **kwargs) -> u.erg * u.cm**(-3) / u.s: contribution_function : Calculate contribution function, :math:`G(n,T)` """ density = np.atleast_1d(density) - pe_ratio = proton_electron_ratio(self.temperature, **self._instance_kwargs) - pe_ratio = pe_ratio[:, np.newaxis, np.newaxis] + pe_ratio = self.proton_electron_ratio[:, np.newaxis, np.newaxis] g = self.contribution_function(density, **kwargs) density_squared = density**2 couple_density_to_temperature = kwargs.get('couple_density_to_temperature', False) diff --git a/fiasco/tests/test_ion.py b/fiasco/tests/test_ion.py index 427aa3c2..94b8c5af 100644 --- a/fiasco/tests/test_ion.py +++ b/fiasco/tests/test_ion.py @@ -582,6 +582,25 @@ def test_ionization_fraction_setter(ion, ioneq_input, ioneq_output): assert u.allclose(ion._instance_kwargs['ionization_fraction'], ioneq_input) +@pytest.mark.parametrize('value', [ + 0.83, + 0.83 * np.ones(temperature.shape), +]) +def test_proton_electron_ratio_setter(ion, value): + ion.proton_electron_ratio = value + assert ion.proton_electron_ratio.shape == ion.temperature.shape + assert u.allclose(ion.proton_electron_ratio, 0.83) + + +@pytest.mark.requires_dbase_version('>= 8') +def test_emissivity_uses_proton_electron_ratio(ion): + # Setting the ratio to 0 should zero the emissivity, which is only the case + # if emissivity uses the (cached) property rather than recomputing the ratio. + ion.proton_electron_ratio = 0.0 + emm = ion.emissivity(1e7 * u.cm**-3) + assert u.allclose(emm, 0 * u.erg / u.cm**3 / u.s) + + def test_ionization_fraction_setter_exception(ion): # This should fail because the input has len>1 but is not the same # shape as the temperature array From 71265d8a4f5a4064d3ab59e3c60be5bf1cfa9c2d Mon Sep 17 00:00:00 2001 From: Will Barnes Date: Wed, 23 Sep 2026 18:47:16 -0400 Subject: [PATCH 02/10] avoid calling plasmapy.particles functions for faster lookups --- fiasco/base.py | 31 ++++++++++++++++++++----------- fiasco/util/tools.py | 26 ++++++++++++++++++++++++++ 2 files changed, 46 insertions(+), 11 deletions(-) diff --git a/fiasco/base.py b/fiasco/base.py index af857f37..fa64647a 100644 --- a/fiasco/base.py +++ b/fiasco/base.py @@ -16,6 +16,7 @@ from fiasco.io.generic import GenericIonParser from fiasco.util import check_database, parse_ion_name, periodic_table_period from fiasco.util.exceptions import MissingIonError +from fiasco.util.tools import _get_atomic_symbol, _get_element_name __all__ = ['IonBase'] @@ -39,20 +40,28 @@ class IonBase: """ def __init__(self, ion_name, hdf5_dbase_root=None, **kwargs): - # base rep is a tuple of integers (atomic_number, ionization_stage) - self._base_rep = parse_ion_name(ion_name) if hdf5_dbase_root is None: self.hdf5_dbase_root = fiasco.defaults['hdf5_dbase_root'] else: self.hdf5_dbase_root = hdf5_dbase_root - check_database(self.hdf5_dbase_root, **kwargs) - if self.ion_name not in fiasco.list_ions(self.hdf5_dbase_root, sort=False): - raise MissingIonError(f'{self.ion_name} not found in {self.hdf5_dbase_root}') # Put import here to avoid circular imports from fiasco import log self.log = log - # Warn users if the database they are using is potentially stale. - self._check_dbase_fiasco_version() + # This optional kwarg is here to avoid costly checks in cases where one + # is sure the database already exists, the ion name is valid and already + # in the right forma, and the version is not potentially stale. This is + # not documented publicly as it is not meant to be used except by other + # internal fiasco functions where the inputs are already validated. + if kwargs.pop('safe_mode', True): + # base rep is a tuple of integers (atomic_number, ionization_stage) + self._base_rep = parse_ion_name(ion_name) + check_database(self.hdf5_dbase_root, **kwargs) + if self.ion_name not in fiasco.list_ions(self.hdf5_dbase_root, sort=False): + raise MissingIonError(f'{self.ion_name} not found in {self.hdf5_dbase_root}') + # Warn users if the database they are using is potentially stale. + self._check_dbase_fiasco_version() + else: + self._base_rep = ion_name def _check_dbase_fiasco_version(self): "Warn if database was generated with an earlier version of fiasco." @@ -70,17 +79,17 @@ def _check_dbase_fiasco_version(self): @property def atomic_number(self): """The atomic number of the element, :math:`Z`.""" - return plasmapy.particles.atomic_number(self._base_rep[0]) + return self._base_rep[0] @property def element_name(self): """The full name of the element, e.g. "hydrogen".""" - return plasmapy.particles.element_name(self.atomic_number) + return _get_element_name(self.atomic_number) @property def atomic_symbol(self): """The standard atomic symbol for the element, e.g. "H" for hydrogen.""" - return plasmapy.particles.atomic_symbol(self.atomic_number) + return _get_atomic_symbol(self.atomic_number) @property def ion_name(self): @@ -101,7 +110,7 @@ def charge_state(self): def isoelectronic_sequence(self): "Atomic symbol denoting to which isoelectronic sequence this ion belongs." if (Z_iso := self.atomic_number - self.charge_state) > 0: - return plasmapy.particles.atomic_symbol(Z_iso) + return _get_atomic_symbol(Z_iso) @property @u.quantity_input diff --git a/fiasco/util/tools.py b/fiasco/util/tools.py index 15f6221b..c81b78cc 100644 --- a/fiasco/util/tools.py +++ b/fiasco/util/tools.py @@ -8,6 +8,10 @@ from functools import partial from scipy.interpolate import splev, splrep +_MAX_Z = 40 +_ATOMIC_SYMBOL_LOOKUP = {z: plasmapy.particles.atomic_symbol(z) for z in range(1, _MAX_Z+1)} +_ELEMENT_NAME_LOOKUP = {z: plasmapy.particles.element_name(z) for z in range(1, _MAX_Z+1)} + __all__ = [ 'vectorize_where', 'vectorize_where_sum', @@ -235,3 +239,25 @@ def periodic_table_period(element): if element <= r: return i+1 raise ValueError(f'No period available for {element=}.') + + +def _get_atomic_symbol(Z): + """ + Return atomic symbol for a given atomic number. + + .. note:: This function is simply a fast version of + plasmapy.particles.atomic_symbol and is only + meant for internal use. + """ + return _ATOMIC_SYMBOL_LOOKUP[Z] + + +def _get_element_name(Z): + """ + Return element name for a given atomic number. + + .. note:: This function is simply a fast version of + plasmapy.particles.element_name and is only meant for + internal use. + """ + return _ELEMENT_NAME_LOOKUP[Z] From 96e1f096967dbc58e131a26904ce4c43dfe8cef6 Mon Sep 17 00:00:00 2001 From: Will Barnes Date: Wed, 23 Sep 2026 18:48:30 -0400 Subject: [PATCH 03/10] significant speedup of listing and ratio functions --- fiasco/fiasco.py | 59 +++++++++++++++++++++++++++++++----------------- 1 file changed, 38 insertions(+), 21 deletions(-) diff --git a/fiasco/fiasco.py b/fiasco/fiasco.py index 27c30978..674ada19 100644 --- a/fiasco/fiasco.py +++ b/fiasco/fiasco.py @@ -48,7 +48,7 @@ def list_elements(hdf5_dbase_root=None, sort=True): return elements -def list_ions(hdf5_dbase_root=None, sort=True): +def list_ions(hdf5_dbase_root=None, sort=True, base_rep=False): """ List all available ions in the CHIANTI database @@ -58,6 +58,10 @@ def list_ions(hdf5_dbase_root=None, sort=True): If not specified, will default to that specified in ``fiasco.defaults``. sort: `bool`, optional If True, sort the list of elements by increasing atomic number. + base_rep: `bool`, optional + If True, return list of ions as tuples of ``(Z, z)``, where ``Z`` is the + atomic number and ``z`` is the ionization stage. This format may be useful + when constructing a large list of `~fiasco.Ion` objects. """ if hdf5_dbase_root is None: hdf5_dbase_root = fiasco.defaults['hdf5_dbase_root'] @@ -82,7 +86,12 @@ def list_ions(hdf5_dbase_root=None, sort=True): # NOTE: when grabbing straight from the index and not sorting, the result will be # a numpy array. Cast to a list to make sure the return type is consistent for # all possible inputs - return ions.tolist() if isinstance(ions, np.ndarray) else ions + ion_list = ions.tolist() if isinstance(ions, np.ndarray) else ions + if base_rep: + # NOTE: Explicitly not using parse_ion_name here as it can be slow. + el_map = {el: plasmapy.particles.atomic_number(el) for el in list_elements(sort=False)} + ion_list = [(el_map[el], int(ion)) for el, ion in map(lambda x: x.split(), ion_list)] + return ion_list def get_dem_model(model, hdf5_dbase_root=None): @@ -156,32 +165,40 @@ def proton_electron_ratio(temperature: u.K, **kwargs): """ # Import here to avoid circular imports from fiasco import log + + # NOTE: Set this to avoid infinite recursion. The exact value is arbitrary because it + # is not used in this calculation. + kwargs['proton_electron_ratio'] = 0.0 h_2 = fiasco.Ion('H +1', temperature, **kwargs) numerator = h_2.abundance * h_2._ion_fraction[h_2._instance_kwargs['ionization_fraction']]['ionization_fraction'] denominator = u.Quantity(np.zeros(numerator.shape)) - for el_name in list_elements(h_2.hdf5_dbase_root): - el = fiasco.Element(el_name, temperature, **h_2._instance_kwargs) + abund_file = h_2._instance_kwargs['abundance'] + ionization_file = h_2._instance_kwargs['ionization_fraction'] + for ion_name in list_ions(hdf5_dbase_root=h_2.hdf5_dbase_root, sort=False, base_rep=True): + # NOTE: Using IonBase to avoid the overhead of repeatedly constructing an Ion object. + ion = fiasco.base.IonBase(ion_name, + hdf5_dbase_root=h_2._instance_kwargs['hdf5_dbase_root'], + safe_mode=False) try: - abundance = el.abundance + abundance = ion._abund[abund_file] except KeyError: - abund_file = el[0]._instance_kwargs['abundance'] log.warning( - f'Not including {el.atomic_symbol}. Abundance not available from {abund_file}.') + f'Not including {ion.ion_name_roman}. Abundance not available from {abund_file}.') continue - for ion in el: - ionization_file = ion._instance_kwargs['ionization_fraction'] - # NOTE: We use ._ion_fraction here rather than .ionization_fraction to avoid - # doing an interpolation to the temperature array every single time and instead only - # interpolate once at the end. - # It is assumed that the ionization_fraction temperature array for each ion is the same. - try: - ionization_fraction = ion._ion_fraction[ionization_file]['ionization_fraction'] - t_ionization_fraction = ion._ion_fraction[ionization_file]['temperature'] - except KeyError: - log.warning( - f'Not including {ion.ion_name}. Ionization fraction not available from {ionization_file}.') - continue - denominator += ionization_fraction * abundance * ion.charge_state + # NOTE: We use ._ion_fraction here rather than .ionization_fraction to avoid + # doing an interpolation to the temperature array every single time and instead only + # interpolate once at the end. + # It is assumed that the ionization_fraction temperature array for each ion is the same. + try: + ion_fraction_ds = ion._ion_fraction[ionization_file] + except KeyError: + log.warning( + f'Not including {ion.ion_name}. Ionization fraction not available from {ionization_file}.') + continue + else: + ionization_fraction = ion_fraction_ds['ionization_fraction'] + t_ionization_fraction = ion_fraction_ds['temperature'] + denominator += ionization_fraction * abundance * ion.charge_state ratio = numerator / denominator f_interp = interp1d(t_ionization_fraction.to(temperature.unit).value, From e9691c5f6ee1d5b1c458ba39018e1d27eaa91642 Mon Sep 17 00:00:00 2001 From: Will Barnes Date: Wed, 23 Sep 2026 18:49:00 -0400 Subject: [PATCH 04/10] refactor setter logic to be more pythonic --- fiasco/ions.py | 27 ++++++++++++++++----------- fiasco/tests/test_ion.py | 7 ++++++- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/fiasco/ions.py b/fiasco/ions.py index 266820ff..31640ed1 100644 --- a/fiasco/ions.py +++ b/fiasco/ions.py @@ -56,6 +56,11 @@ class Ion(IonBase): ionization_potential : `str` or `~astropy.units.Quantity`, optional If a string is provided, use the appropriate "ip" dataset. If a scalar value is provided, use that value for the ionization potential. This value should be convertible to eV. + proton_electron_ratio : `float` or array-like, optional + Ratio of proton to electron densities, :math:`n_H/n_e`, as a function of temperature. Can be a scalar value or + an array with the same shape as ``temperature``. If not specified, this is calculated using + `~fiasco.proton_electron_ratio`. When instantiating many ions, it may be more efficient to precompute this + quantity and then pass it to the constructor through this keyword argument. """ @u.quantity_input @@ -65,15 +70,16 @@ def __init__(self, abundance='sun_coronal_1992_feldman_ext', ionization_fraction='chianti', ionization_potential='chianti', + proton_electron_ratio=None, *args, **kwargs): super().__init__(ion_name, *args, **kwargs) self.temperature = np.atleast_1d(temperature) self._dset_names = {} - self._proton_electron_ratio = None self.abundance = abundance self.ionization_fraction = ionization_fraction self.ionization_potential = ionization_potential + self.proton_electron_ratio = proton_electron_ratio self.gaunt_factor = GauntFactor(hdf5_dbase_root=self.hdf5_dbase_root) def _new_instance(self, temperature=None, **kwargs): @@ -147,6 +153,12 @@ def _instance_kwargs(self): kwargs['ionization_fraction'] = self.ionization_fraction if kwargs['ionization_potential'] is None: kwargs['ionization_potential'] = self.ionization_potential + # NOTE: It is possible that this property is needed prior to the proton/electron ratio + # being set. This logic guards against that. + try: + kwargs['proton_electron_ratio'] = self.proton_electron_ratio + except AttributeError: + self.log.debug('Proton/electron ratio not added to instance kwargs.') return kwargs def _has_dataset(self, dset_name): @@ -209,21 +221,14 @@ def thermal_energy(self) -> u.erg: def proton_electron_ratio(self) -> u.dimensionless_unscaled: """ Ratio of proton to electron number density as a function of temperature. - - Computed with `fiasco.proton_electron_ratio` on first access and cached afterward. - The ratio depends only on the temperature and the abundance and ionization fraction - datasets, so it can also be set directly to avoid recomputing it for every ion that - shares the same temperature, e.g. - ``ion.proton_electron_ratio = fiasco.proton_electron_ratio(ion.temperature)``. """ - if self._proton_electron_ratio is None: - self._proton_electron_ratio = proton_electron_ratio(self.temperature, **self._instance_kwargs) return self._proton_electron_ratio @proton_electron_ratio.setter def proton_electron_ratio(self, value): - # Multiplying by np.ones allows for passing in scalar values - self._proton_electron_ratio = np.atleast_1d(value) * np.ones(self.temperature.shape) + if value is None: + value = proton_electron_ratio(self.temperature, **self._instance_kwargs) + self._proton_electron_ratio = u.Quantity(value*np.ones(self.temperature.shape)) def next_ion(self): """ diff --git a/fiasco/tests/test_ion.py b/fiasco/tests/test_ion.py index 94b8c5af..ac05cff6 100644 --- a/fiasco/tests/test_ion.py +++ b/fiasco/tests/test_ion.py @@ -589,7 +589,12 @@ def test_ionization_fraction_setter(ion, ioneq_input, ioneq_output): def test_proton_electron_ratio_setter(ion, value): ion.proton_electron_ratio = value assert ion.proton_electron_ratio.shape == ion.temperature.shape - assert u.allclose(ion.proton_electron_ratio, 0.83) + assert u.allclose(ion.proton_electron_ratio, value) + new_ion = fiasco.Ion(ion.ion_name, + ion.temperature, + proton_electron_ratio=value) + assert new_ion.proton_electron_ratio.shape == new_ion.temperature.shape + assert u.allclose(new_ion.proton_electron_ratio, value) @pytest.mark.requires_dbase_version('>= 8') From f631e396badacda0d4a72ab008dc8a0de34976ed Mon Sep 17 00:00:00 2001 From: Will Barnes Date: Wed, 23 Sep 2026 18:49:29 -0400 Subject: [PATCH 05/10] only calculate p/e ratio once when building Element object of ions --- fiasco/elements.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/fiasco/elements.py b/fiasco/elements.py index 37c201db..7917e6aa 100644 --- a/fiasco/elements.py +++ b/fiasco/elements.py @@ -39,6 +39,9 @@ def __init__(self, element_name, temperature: u.K, **kwargs): element_name = element_name.capitalize() Z = plasmapy.particles.atomic_number(element_name) ion_list = [] + if (pe_ratio := kwargs.pop('proton_electron_ratio', None)) is None: + pe_ratio = fiasco.proton_electron_ratio(temperature, **kwargs) + kwargs['proton_electron_ratio'] = pe_ratio for i in range(Z + 1): ion = fiasco.Ion((Z, i+1), temperature, **kwargs) ion_list.append(ion) From c2b09f9546e62a94f73a9ded4177b075b7044fcb Mon Sep 17 00:00:00 2001 From: Will Barnes Date: Wed, 23 Sep 2026 23:01:25 -0400 Subject: [PATCH 06/10] pass database path through to list_elements --- fiasco/fiasco.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fiasco/fiasco.py b/fiasco/fiasco.py index 674ada19..1a50ca2b 100644 --- a/fiasco/fiasco.py +++ b/fiasco/fiasco.py @@ -89,7 +89,8 @@ def list_ions(hdf5_dbase_root=None, sort=True, base_rep=False): ion_list = ions.tolist() if isinstance(ions, np.ndarray) else ions if base_rep: # NOTE: Explicitly not using parse_ion_name here as it can be slow. - el_map = {el: plasmapy.particles.atomic_number(el) for el in list_elements(sort=False)} + elements = list_elements(hdf5_dbase_root=hdf5_dbase_root, sort=False) + el_map = {el: plasmapy.particles.atomic_number(el) for el in elements} ion_list = [(el_map[el], int(ion)) for el, ion in map(lambda x: x.split(), ion_list)] return ion_list From bf4614fa5e9f83e34346b4f0ab76042dea57f1ad Mon Sep 17 00:00:00 2001 From: Will Barnes Date: Wed, 23 Sep 2026 23:50:12 -0400 Subject: [PATCH 07/10] avoid very slow IonCollection creation --- examples/idl_comparisons/continuum_comparison.py | 9 ++++++--- fiasco/tests/idl/test_idl_continuum.py | 3 +++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/examples/idl_comparisons/continuum_comparison.py b/examples/idl_comparisons/continuum_comparison.py index 219d3df9..bc05cc1e 100644 --- a/examples/idl_comparisons/continuum_comparison.py +++ b/examples/idl_comparisons/continuum_comparison.py @@ -65,7 +65,8 @@ def plot_idl_comparison(wavelength, temperature, result_fiasco, result_idl): # thermal bremsstrahlung. idl_result_freefree = read_idl_test_output('freefree_all_ions', LATEST_VERSION) ion_kwargs = {'abundance': idl_result_freefree['abundance'], - 'ionization_fraction': idl_result_freefree['ionization_fraction']} + 'ionization_fraction': idl_result_freefree['ionization_fraction'], + 'proton_electron_ratio': 0.83} all_ions = [fiasco.Ion(ion_name, idl_result_freefree['temperature'], **ion_kwargs) for ion_name in fiasco.list_ions()] all_ions = fiasco.IonCollection(*all_ions) free_free = all_ions.free_free(idl_result_freefree['wavelength']) @@ -83,7 +84,8 @@ def plot_idl_comparison(wavelength, temperature, result_fiasco, result_idl): # continuum emission. idl_result_freebound = read_idl_test_output('freebound_all_ions', LATEST_VERSION) ion_kwargs = {'abundance': idl_result_freebound['abundance'], - 'ionization_fraction': idl_result_freebound['ionization_fraction']} + 'ionization_fraction': idl_result_freebound['ionization_fraction'], + 'proton_electron_ratio': 0.83} all_ions = [fiasco.Ion(ion_name, idl_result_freebound['temperature'], **ion_kwargs) for ion_name in fiasco.list_ions()] all_ions = fiasco.IonCollection(*all_ions) free_bound = all_ions.free_bound(idl_result_freebound['wavelength']) @@ -101,7 +103,8 @@ def plot_idl_comparison(wavelength, temperature, result_fiasco, result_idl): # continuum emission. idl_result_twophoton = read_idl_test_output('twophoton_all_ions', LATEST_VERSION) ion_kwargs = {'abundance': idl_result_twophoton['abundance'], - 'ionization_fraction': idl_result_twophoton['ionization_fraction']} + 'ionization_fraction': idl_result_twophoton['ionization_fraction'], + 'proton_electron_ratio': 0.83} all_ions = [fiasco.Ion(ion_name, idl_result_twophoton['temperature'], **ion_kwargs) for ion_name in fiasco.list_ions()] all_ions = fiasco.IonCollection(*all_ions) two_photon = all_ions.two_photon(idl_result_twophoton['wavelength'], diff --git a/fiasco/tests/idl/test_idl_continuum.py b/fiasco/tests/idl/test_idl_continuum.py index 335d2905..df36ff9d 100644 --- a/fiasco/tests/idl/test_idl_continuum.py +++ b/fiasco/tests/idl/test_idl_continuum.py @@ -22,6 +22,9 @@ def ion_input_args(): return { 'abundance': 'sun_coronal_1992_feldman_ext', 'ionization_fraction': 'chianti', + # NOTE: this isn't used anywhere in the continuum calculations + # so just set it to a scalar value for efficiency. + 'proton_electron_ratio': 0.83, } From d4ccce2280c522898719fda7d4653325f1130e2e Mon Sep 17 00:00:00 2001 From: Will Barnes Date: Thu, 24 Sep 2026 00:55:13 -0400 Subject: [PATCH 08/10] fix more failing tests --- fiasco/tests/data/test_file_list.json | 1 + fiasco/tests/test_ion.py | 18 +++++++++--------- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/fiasco/tests/data/test_file_list.json b/fiasco/tests/data/test_file_list.json index 6f853f08..aa801929 100644 --- a/fiasco/tests/data/test_file_list.json +++ b/fiasco/tests/data/test_file_list.json @@ -151,6 +151,7 @@ "ar_18.scups", "ar_18.wgfa", "ar_19.rrparams", + "archive/sun_coronal_1992_feldman.abund", "archive/sun_coronal_1992_feldman_ext.abund", "archive/sun_photospheric_2007_grevesse.abund", "b_1.diparams", diff --git a/fiasco/tests/test_ion.py b/fiasco/tests/test_ion.py index ac05cff6..17e066e3 100644 --- a/fiasco/tests/test_ion.py +++ b/fiasco/tests/test_ion.py @@ -70,7 +70,9 @@ def test_new_instance(ion): abundance = ion._instance_kwargs['abundance'] new_ion = ion._new_instance() for k in new_ion._instance_kwargs: - assert new_ion._instance_kwargs[k] == ion._instance_kwargs[k] + if k != 'proton_electron_ratio': + assert new_ion._instance_kwargs[k] == ion._instance_kwargs[k] + assert u.allclose(new_ion.proton_electron_ratio, ion.proton_electron_ratio, rtol=0) assert u.allclose(new_ion.temperature, ion.temperature, rtol=0) new_ion = ion._new_instance(temperature=ion.temperature[:1]) assert u.allclose(new_ion.temperature, ion.temperature[:1]) @@ -194,7 +196,7 @@ def test_no_elvlc_raises_index_error(hdf5_dbase_root): def test_ionization_fraction(ion): t_data = ion._ion_fraction[ion._dset_names['ionization_fraction']]['temperature'] ionization_data = ion._ion_fraction[ion._dset_names['ionization_fraction']]['ionization_fraction'] - ion_at_nodes = ion._new_instance(temperature=t_data) + ion_at_nodes = ion._new_instance(temperature=t_data, proton_electron_ratio=1) assert u.allclose(ion_at_nodes.ionization_fraction, ionization_data, rtol=1e-6) @@ -205,7 +207,7 @@ def test_ionization_fraction_positive(ion): def test_ionization_fraction_out_bounds_is_nan(ion): t_data = ion._ion_fraction[ion._dset_names['ionization_fraction']]['temperature'] t_out_of_bounds = t_data[[0,-1]] + [-100, 1e6] * u.K - ion_out_of_bounds = ion._new_instance(temperature=t_out_of_bounds) + ion_out_of_bounds = ion._new_instance(temperature=t_out_of_bounds, proton_electron_ratio=1) assert np.isnan(ion_out_of_bounds.ionization_fraction).all() @@ -230,9 +232,9 @@ def test_proton_collision(fe10): def test_missing_abundance(hdf5_dbase_root): _ion = fiasco.Ion('Li 1', - temperature, - abundance='sun_coronal_1992_feldman', - hdf5_dbase_root=hdf5_dbase_root) + temperature, + abundance='sun_coronal_1992_feldman', + hdf5_dbase_root=hdf5_dbase_root) with pytest.raises(MissingDatasetException): _ion.abundance @@ -590,9 +592,7 @@ def test_proton_electron_ratio_setter(ion, value): ion.proton_electron_ratio = value assert ion.proton_electron_ratio.shape == ion.temperature.shape assert u.allclose(ion.proton_electron_ratio, value) - new_ion = fiasco.Ion(ion.ion_name, - ion.temperature, - proton_electron_ratio=value) + new_ion = ion._new_instance(proton_electron_ratio=value) assert new_ion.proton_electron_ratio.shape == new_ion.temperature.shape assert u.allclose(new_ion.proton_electron_ratio, value) From b4832f68183a94d48fdd15a897206ba9ab08062e Mon Sep 17 00:00:00 2001 From: Will Barnes Date: Thu, 24 Sep 2026 09:34:15 -0400 Subject: [PATCH 09/10] remove changelog --- changelog/471.bugfix.rst | 1 - 1 file changed, 1 deletion(-) delete mode 100644 changelog/471.bugfix.rst diff --git a/changelog/471.bugfix.rst b/changelog/471.bugfix.rst deleted file mode 100644 index 504858b8..00000000 --- a/changelog/471.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -`fiasco.Ion.emissivity` now uses the cached `fiasco.Ion.proton_electron_ratio` property rather than recomputing the proton-to-electron ratio, which walks the entire database, on every call. `fiasco.Ion.proton_electron_ratio` can now also be set directly so that a ratio computed once with `fiasco.proton_electron_ratio` can be shared between ions with the same temperature. From 7e3b8d39a2ce3ac3089824e7bd5964a05c865f2c Mon Sep 17 00:00:00 2001 From: Will Barnes Date: Thu, 24 Sep 2026 09:34:41 -0400 Subject: [PATCH 10/10] account for changing temperature grid when propagating p/e ratio --- fiasco/ions.py | 7 ++++++- fiasco/tests/test_ion.py | 4 ++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/fiasco/ions.py b/fiasco/ions.py index 31640ed1..5d7ebc89 100644 --- a/fiasco/ions.py +++ b/fiasco/ions.py @@ -88,9 +88,14 @@ def _new_instance(self, temperature=None, **kwargs): possibly different arguments. If different arguments are not specified, this will just create a copy of itself. """ + new_kwargs = self._instance_kwargs if temperature is None: temperature = self.temperature.copy() - new_kwargs = self._instance_kwargs + else: + # If a new temperature array is specified, this could now be stale + # so we remove the old value and force it to be recomputed on the + # updated temperature array if a new value is not specified. + new_kwargs.pop('proton_electron_ratio', None) new_kwargs.update(kwargs) return type(self)(self.ion_name, temperature, **new_kwargs) diff --git a/fiasco/tests/test_ion.py b/fiasco/tests/test_ion.py index 17e066e3..f12d059f 100644 --- a/fiasco/tests/test_ion.py +++ b/fiasco/tests/test_ion.py @@ -196,7 +196,7 @@ def test_no_elvlc_raises_index_error(hdf5_dbase_root): def test_ionization_fraction(ion): t_data = ion._ion_fraction[ion._dset_names['ionization_fraction']]['temperature'] ionization_data = ion._ion_fraction[ion._dset_names['ionization_fraction']]['ionization_fraction'] - ion_at_nodes = ion._new_instance(temperature=t_data, proton_electron_ratio=1) + ion_at_nodes = ion._new_instance(temperature=t_data) assert u.allclose(ion_at_nodes.ionization_fraction, ionization_data, rtol=1e-6) @@ -207,7 +207,7 @@ def test_ionization_fraction_positive(ion): def test_ionization_fraction_out_bounds_is_nan(ion): t_data = ion._ion_fraction[ion._dset_names['ionization_fraction']]['temperature'] t_out_of_bounds = t_data[[0,-1]] + [-100, 1e6] * u.K - ion_out_of_bounds = ion._new_instance(temperature=t_out_of_bounds, proton_electron_ratio=1) + ion_out_of_bounds = ion._new_instance(temperature=t_out_of_bounds) assert np.isnan(ion_out_of_bounds.ionization_fraction).all()