From b18bf1b811d6eaa19942440277dac2a0b29c75cf Mon Sep 17 00:00:00 2001 From: jacobdparker Date: Thu, 24 Sep 2026 10:00:32 -0600 Subject: [PATCH 01/12] Add `IonCollection.build_line_list` A table of the bound-bound transitions of every ion in a collection, one row per line, with the contribution function of each line as a function of temperature and density. Ions can be excluded by a minimum abundance or by having no line in a wavelength range, and if a DEM model in the form returned by `get_dem_model` is given the intensity of each line is included. Closes #464 Co-Authored-By: Claude Fable 5.1 --- changelog/472.feature.rst | 1 + fiasco/collections.py | 122 +++++++++++++++++++++++++++++++ fiasco/tests/test_collections.py | 54 ++++++++++++++ 3 files changed, 177 insertions(+) create mode 100644 changelog/472.feature.rst diff --git a/changelog/472.feature.rst b/changelog/472.feature.rst new file mode 100644 index 00000000..5a78b4bb --- /dev/null +++ b/changelog/472.feature.rst @@ -0,0 +1 @@ +Added `fiasco.IonCollection.build_line_list` for building a table of the emission lines of every ion in a collection, optionally restricted to a wavelength range and a minimum abundance, with the contribution function of each line and, if a DEM model is given, its intensity. diff --git a/fiasco/collections.py b/fiasco/collections.py index f96cc844..f0b55164 100644 --- a/fiasco/collections.py +++ b/fiasco/collections.py @@ -6,6 +6,8 @@ from astropy.convolution import convolve, Model1DKernel from astropy.modeling.models import Gaussian1D +from astropy.table import QTable, vstack +from scipy.interpolate import interp1d import fiasco @@ -214,6 +216,126 @@ def two_photon(self, wavelength: u.angstrom, electron_density: u.cm**-3, **kwarg two_photon += tp * ion.abundance * ion.ionization_fraction[:, np.newaxis, np.newaxis] return two_photon + @u.quantity_input + def build_line_list(self, + density: u.cm**(-3), + minimum_abundance=0, + wavelength_range: u.angstrom = None, + emission_measure=None, + **kwargs): + r""" + Build a table of the emission lines of all ions in the collection. + + Each row of the table is one bound-bound transition of one ion in the + collection, together with the contribution function of that transition + as a function of temperature and density. Ions without the data needed to + compute the contribution function are skipped with a warning. + + Parameters + ---------- + density: `~astropy.units.Quantity` + Electron number density used to compute the contribution function. + minimum_abundance: `float`, optional + Ions of elements with an abundance below this value are not included. + wavelength_range: `~astropy.units.Quantity`, optional + Two-element quantity giving the lower and upper bounds on the wavelengths of + transitions to include. Ions with no transition in this range are skipped. + Default is to include all transitions. + emission_measure: `~astropy.table.QTable`, optional + Differential emission measure model in the form returned by + `fiasco.get_dem_model`, with ``temperature_bin_center`` and ``em`` columns. + If given, the line-of-sight intensity of each transition is included in the + table as well. The contribution function is interpolated (in :math:`\log T`) + to the temperature bin centers of the model and summed over the bins; bins + outside the temperature range of the collection contribute nothing. + couple_density_to_temperature: `bool`, optional + If True, the density will vary along the same axis as temperature. + See `fiasco.Ion.contribution_function`. + + Returns + ------- + : `~astropy.table.QTable` + One row per transition with the columns ``element``, ``atomic_number``, + ``ion_name``, ``abundance``, ``ionization_potential``, ``wavelength``, + ``energy``, ``A``, ``upper_level``, ``lower_level``, ``upper_label``, + ``lower_label``, ``is_observed``, and ``contribution_function``, plus + ``intensity`` if ``emission_measure`` is + given. The ``contribution_function`` column has an entry of shape ``(l, m)`` + per row, where ``l`` is the number of temperatures and ``m`` is the number of + densities (or 1 if ``couple_density_to_temperature=True``), and ``intensity`` + has an entry of shape ``(m,)``. The temperature and density are stored in + the ``meta`` of the table. + + See Also + -------- + fiasco.Ion.contribution_function + fiasco.get_dem_model + """ + density = np.atleast_1d(density) + if wavelength_range is None: + wavelength_range = u.Quantity([0, np.inf], 'angstrom') + if emission_measure is not None: + log_temperature = np.log10(self.temperature.to_value('K')) + log_temperature_dem = np.log10(emission_measure['temperature_bin_center'].to_value('K')) + em = emission_measure['em'] + tables = [] + for ion in self: + try: + abundance = ion.abundance + transitions = ion.transitions + except MissingDatasetException as e: + self.log.warning(f'{ion.ion_name} not included in line list. {e}') + continue + if abundance < minimum_abundance: + continue + is_bound_bound = transitions.is_bound_bound + wavelength = transitions.wavelength[is_bound_bound] + in_range = np.logical_and(wavelength >= wavelength_range[0], + wavelength <= wavelength_range[1]) + if not in_range.any(): + continue + try: + g = ion.contribution_function(density, **kwargs) + except MissingDatasetException as e: + self.log.warning(f'{ion.ion_name} not included in line list. {e}') + continue + try: + ionization_potential = ion.ionization_potential + except MissingDatasetException: + ionization_potential = np.nan * u.eV + # Put the transition axis first so that each row of the table holds the + # contribution function of one transition + g = np.moveaxis(g[..., in_range], -1, 0) + n_lines = in_range.sum() + table = QTable({ + 'element': [ion.atomic_symbol] * n_lines, + 'atomic_number': [ion.atomic_number] * n_lines, + 'ion_name': [ion.ion_name_roman] * n_lines, + 'abundance': [abundance] * n_lines, + 'ionization_potential': u.Quantity([ionization_potential] * n_lines), + 'wavelength': wavelength[in_range], + 'energy': wavelength[in_range].to('eV', equivalencies=u.spectral()), + 'A': transitions.A[is_bound_bound][in_range], + 'upper_level': transitions.upper_level[is_bound_bound][in_range], + 'lower_level': transitions.lower_level[is_bound_bound][in_range], + 'upper_label': transitions.upper_label[is_bound_bound][in_range], + 'lower_label': transitions.lower_label[is_bound_bound][in_range], + 'is_observed': transitions.is_observed[is_bound_bound][in_range], + 'contribution_function': g, + }) + if emission_measure is not None: + f_interp = interp1d(log_temperature, g.value, axis=1, bounds_error=False, fill_value=0.0) + g_dem = f_interp(log_temperature_dem) * g.unit + intensity = (g_dem * em[np.newaxis, :, np.newaxis]).sum(axis=1) + table['intensity'] = intensity / (4*np.pi*u.steradian) + tables.append(table) + if not tables: + raise ValueError('No lines found for any ion in collection.') + line_list = vstack(tables) + line_list.meta['temperature'] = self.temperature + line_list.meta['density'] = density + return line_list + @u.quantity_input def spectrum(self, density: u.cm**(-3), emission_measure: u.cm**(-5), wavelength_range=None, bin_width=None, kernel=None, **kwargs): diff --git a/fiasco/tests/test_collections.py b/fiasco/tests/test_collections.py index 34400728..692118d5 100644 --- a/fiasco/tests/test_collections.py +++ b/fiasco/tests/test_collections.py @@ -198,6 +198,60 @@ def test_spectrum(hdf5_dbase_root): assert np.all(spec == spec2) +@pytest.mark.requires_dbase_version('>= 8') +def test_build_line_list(hdf5_dbase_root): + h1 = fiasco.Ion('H 1', temperature, hdf5_dbase_root=hdf5_dbase_root) + fe5 = fiasco.Ion('Fe 5', temperature, hdf5_dbase_root=hdf5_dbase_root) + # H 2 has no transition data and should be skipped + h2 = fiasco.Ion('H 2', temperature, hdf5_dbase_root=hdf5_dbase_root) + c = h1 + fe5 + h2 + density = 1e9 * u.cm**-3 + line_list = c.build_line_list(density) + n_h1 = h1.transitions.is_bound_bound.sum() + n_fe5 = fe5.transitions.is_bound_bound.sum() + assert len(line_list) == n_h1 + n_fe5 + assert set(line_list['ion_name']) == {'H I', 'Fe V'} + assert line_list['contribution_function'].shape == (n_h1 + n_fe5, temperature.shape[0], 1) + assert 'intensity' not in line_list.colnames + assert u.allclose(line_list.meta['temperature'], temperature) + # Level indices point back at the levels of the ion + is_fe5 = line_list['ion_name'] == 'Fe V' + assert np.array_equal(line_list['upper_level'][is_fe5], + fe5.transitions.upper_level[fe5.transitions.is_bound_bound]) + assert np.all(line_list['upper_level'] > line_list['lower_level']) + # Consistent with the contribution function of each ion + g_fe5 = fe5.contribution_function(density) + assert u.allclose(line_list['contribution_function'][line_list['ion_name'] == 'Fe V'], + np.moveaxis(g_fe5, -1, 0)) + # Selecting by abundance removes iron + line_list = c.build_line_list(density, minimum_abundance=1e-3) + assert set(line_list['ion_name']) == {'H I'} + # Selecting by wavelength range keeps only the lines in range + wavelength_range = [900, 1300] * u.angstrom + line_list = c.build_line_list(density, wavelength_range=wavelength_range) + assert np.all(line_list['wavelength'] >= wavelength_range[0]) + assert np.all(line_list['wavelength'] <= wavelength_range[1]) + assert 'H I' in set(line_list['ion_name']) + + +@pytest.mark.requires_dbase_version('>= 8') +def test_build_line_list_intensity(hdf5_dbase_root): + fe5 = fiasco.Ion('Fe 5', temperature, hdf5_dbase_root=hdf5_dbase_root) + c = fiasco.IonCollection(fe5) + density = [1e9, 1e10] * u.cm**-3 + dem = fiasco.get_dem_model('quiet_sun', hdf5_dbase_root=hdf5_dbase_root) + line_list = c.build_line_list(density, emission_measure=dem) + assert line_list['intensity'].shape == (len(line_list), 2) + assert line_list['intensity'].unit.is_equivalent(u.erg / u.cm**2 / u.s / u.steradian) + assert np.all(line_list['intensity'] >= 0) + + +def test_build_line_list_no_valid_ions(hdf5_dbase_root): + c = fiasco.IonCollection(fiasco.Ion('H 2', temperature, hdf5_dbase_root=hdf5_dbase_root)) + with pytest.raises(ValueError, match='No lines found for any ion in collection.'): + c.build_line_list(1e9 * u.cm**-3) + + def test_spectrum_no_valid_ions(hdf5_dbase_root): # Consider the case of an collection with ions with no spectral information c2 = fiasco.IonCollection(fiasco.Ion('H 2', 1 * u.MK, hdf5_dbase_root=hdf5_dbase_root)) From cfdeb3ce7ce5b2d392099bd0c8eba1f0062ea642 Mon Sep 17 00:00:00 2001 From: jacobdparker Date: Tue, 6 Oct 2026 16:18:58 -0600 Subject: [PATCH 02/12] Address review of `build_line_list` - One `line_list` fixture and a separate test for each concern - Document that any kwarg of `contribution_function` passes through, and stop listing every column - No guard on the ionization potential - `energy` from the level energies rather than the wavelength - Dataset names, CHIANTI version and fiasco version in `meta` Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01SPbZHPLHsKKTtmMueYx5RM --- fiasco/collections.py | 39 ++++++++++---------- fiasco/tests/test_collections.py | 63 +++++++++++++++++++++++++------- 2 files changed, 70 insertions(+), 32 deletions(-) diff --git a/fiasco/collections.py b/fiasco/collections.py index f0b55164..cc57c089 100644 --- a/fiasco/collections.py +++ b/fiasco/collections.py @@ -248,23 +248,20 @@ def build_line_list(self, table as well. The contribution function is interpolated (in :math:`\log T`) to the temperature bin centers of the model and summed over the bins; bins outside the temperature range of the collection contribute nothing. - couple_density_to_temperature: `bool`, optional - If True, the density will vary along the same axis as temperature. - See `fiasco.Ion.contribution_function`. + kwargs: `dict`, optional + Any keyword argument accepted by `fiasco.Ion.contribution_function`, such as + ``couple_density_to_temperature``, is passed through to it. Returns ------- : `~astropy.table.QTable` - One row per transition with the columns ``element``, ``atomic_number``, - ``ion_name``, ``abundance``, ``ionization_potential``, ``wavelength``, - ``energy``, ``A``, ``upper_level``, ``lower_level``, ``upper_label``, - ``lower_label``, ``is_observed``, and ``contribution_function``, plus - ``intensity`` if ``emission_measure`` is - given. The ``contribution_function`` column has an entry of shape ``(l, m)`` - per row, where ``l`` is the number of temperatures and ``m`` is the number of - densities (or 1 if ``couple_density_to_temperature=True``), and ``intensity`` - has an entry of shape ``(m,)``. The temperature and density are stored in - the ``meta`` of the table. + One row per transition, with the properties of the transition and of the ion + it belongs to as columns. The ``contribution_function`` column has an entry + of shape ``(l, m)`` per row, where ``l`` is the number of temperatures and + ``m`` is the number of densities (or 1 if ``couple_density_to_temperature=True``), + and ``intensity``, present if ``emission_measure`` is given, has an entry of + shape ``(m,)``. The temperature, density, dataset names, and database and + fiasco versions are stored in the ``meta`` of the table. See Also -------- @@ -279,6 +276,7 @@ def build_line_list(self, log_temperature_dem = np.log10(emission_measure['temperature_bin_center'].to_value('K')) em = emission_measure['em'] tables = [] + included = [] for ion in self: try: abundance = ion.abundance @@ -299,10 +297,6 @@ def build_line_list(self, except MissingDatasetException as e: self.log.warning(f'{ion.ion_name} not included in line list. {e}') continue - try: - ionization_potential = ion.ionization_potential - except MissingDatasetException: - ionization_potential = np.nan * u.eV # Put the transition axis first so that each row of the table holds the # contribution function of one transition g = np.moveaxis(g[..., in_range], -1, 0) @@ -312,9 +306,9 @@ def build_line_list(self, 'atomic_number': [ion.atomic_number] * n_lines, 'ion_name': [ion.ion_name_roman] * n_lines, 'abundance': [abundance] * n_lines, - 'ionization_potential': u.Quantity([ionization_potential] * n_lines), + 'ionization_potential': u.Quantity([ion.ionization_potential] * n_lines), 'wavelength': wavelength[in_range], - 'energy': wavelength[in_range].to('eV', equivalencies=u.spectral()), + 'energy': transitions.delta_energy[is_bound_bound][in_range], 'A': transitions.A[is_bound_bound][in_range], 'upper_level': transitions.upper_level[is_bound_bound][in_range], 'lower_level': transitions.lower_level[is_bound_bound][in_range], @@ -329,11 +323,18 @@ def build_line_list(self, intensity = (g_dem * em[np.newaxis, :, np.newaxis]).sum(axis=1) table['intensity'] = intensity / (4*np.pi*u.steradian) tables.append(table) + included.append(ion) if not tables: raise ValueError('No lines found for any ion in collection.') line_list = vstack(tables) line_list.meta['temperature'] = self.temperature line_list.meta['density'] = density + for key in ('abundance', 'ionization_fraction'): + # the name is None for an ion whose dataset was set to a value directly + names = sorted({ion._dset_names[key] for ion in included}, key=str) + line_list.meta[key] = names[0] if len(names) == 1 else names + line_list.meta['chianti_version'] = str(included[0]._elvlc.version) + line_list.meta['fiasco_version'] = fiasco.__version__ return line_list @u.quantity_input diff --git a/fiasco/tests/test_collections.py b/fiasco/tests/test_collections.py index 692118d5..3b69b74f 100644 --- a/fiasco/tests/test_collections.py +++ b/fiasco/tests/test_collections.py @@ -198,37 +198,74 @@ def test_spectrum(hdf5_dbase_root): assert np.all(spec == spec2) -@pytest.mark.requires_dbase_version('>= 8') -def test_build_line_list(hdf5_dbase_root): +line_list_density = 1e9 * u.cm**-3 + + +@pytest.fixture +def line_list_collection(hdf5_dbase_root): h1 = fiasco.Ion('H 1', temperature, hdf5_dbase_root=hdf5_dbase_root) fe5 = fiasco.Ion('Fe 5', temperature, hdf5_dbase_root=hdf5_dbase_root) # H 2 has no transition data and should be skipped h2 = fiasco.Ion('H 2', temperature, hdf5_dbase_root=hdf5_dbase_root) - c = h1 + fe5 + h2 - density = 1e9 * u.cm**-3 - line_list = c.build_line_list(density) + return h1 + fe5 + h2 + + +@pytest.fixture +def line_list(line_list_collection): + return line_list_collection.build_line_list(line_list_density) + + +@pytest.mark.requires_dbase_version('>= 8') +def test_build_line_list_rows(line_list, line_list_collection): + h1, fe5, _ = line_list_collection n_h1 = h1.transitions.is_bound_bound.sum() n_fe5 = fe5.transitions.is_bound_bound.sum() assert len(line_list) == n_h1 + n_fe5 assert set(line_list['ion_name']) == {'H I', 'Fe V'} - assert line_list['contribution_function'].shape == (n_h1 + n_fe5, temperature.shape[0], 1) assert 'intensity' not in line_list.colnames - assert u.allclose(line_list.meta['temperature'], temperature) - # Level indices point back at the levels of the ion + + +@pytest.mark.requires_dbase_version('>= 8') +def test_build_line_list_levels(line_list, line_list_collection): + _, fe5, _ = line_list_collection is_fe5 = line_list['ion_name'] == 'Fe V' assert np.array_equal(line_list['upper_level'][is_fe5], fe5.transitions.upper_level[fe5.transitions.is_bound_bound]) assert np.all(line_list['upper_level'] > line_list['lower_level']) - # Consistent with the contribution function of each ion - g_fe5 = fe5.contribution_function(density) + assert np.all(line_list['energy'] > 0 * u.eV) + + +@pytest.mark.requires_dbase_version('>= 8') +def test_build_line_list_contribution_function(line_list, line_list_collection): + _, fe5, _ = line_list_collection + assert line_list['contribution_function'].shape == (len(line_list), temperature.shape[0], 1) + g_fe5 = fe5.contribution_function(line_list_density) assert u.allclose(line_list['contribution_function'][line_list['ion_name'] == 'Fe V'], np.moveaxis(g_fe5, -1, 0)) + + +@pytest.mark.requires_dbase_version('>= 8') +def test_build_line_list_meta(line_list, line_list_collection): + h1, _, _ = line_list_collection + assert u.allclose(line_list.meta['temperature'], temperature) + assert u.allclose(line_list.meta['density'], line_list_density) + assert line_list.meta['abundance'] == h1._dset_names['abundance'] + assert line_list.meta['ionization_fraction'] == h1._dset_names['ionization_fraction'] + assert line_list.meta['chianti_version'] == str(h1._elvlc.version) + assert line_list.meta['fiasco_version'] == fiasco.__version__ + + +@pytest.mark.requires_dbase_version('>= 8') +def test_build_line_list_minimum_abundance(line_list_collection): # Selecting by abundance removes iron - line_list = c.build_line_list(density, minimum_abundance=1e-3) + line_list = line_list_collection.build_line_list(line_list_density, minimum_abundance=1e-3) assert set(line_list['ion_name']) == {'H I'} - # Selecting by wavelength range keeps only the lines in range + + +@pytest.mark.requires_dbase_version('>= 8') +def test_build_line_list_wavelength_range(line_list_collection): wavelength_range = [900, 1300] * u.angstrom - line_list = c.build_line_list(density, wavelength_range=wavelength_range) + line_list = line_list_collection.build_line_list(line_list_density, wavelength_range=wavelength_range) assert np.all(line_list['wavelength'] >= wavelength_range[0]) assert np.all(line_list['wavelength'] <= wavelength_range[1]) assert 'H I' in set(line_list['ion_name']) From dc0b9f76b1de88e71501e5108020808b3ee2e756 Mon Sep 17 00:00:00 2001 From: jacobdparker Date: Fri, 23 Oct 2026 09:50:41 -0700 Subject: [PATCH 03/12] Log skipped ions in `build_line_list` and remove changelog Ions dropped by `minimum_abundance` or `wavelength_range` are now logged at info level, with a test. Comments explain how the dataset names in `meta` are collected and why H II never appears in the line list. Co-Authored-By: Claude Opus 5.5 --- changelog/472.feature.rst | 1 - fiasco/collections.py | 8 +++++++- fiasco/tests/test_collections.py | 15 ++++++++++++++- 3 files changed, 21 insertions(+), 3 deletions(-) delete mode 100644 changelog/472.feature.rst diff --git a/changelog/472.feature.rst b/changelog/472.feature.rst deleted file mode 100644 index 5a78b4bb..00000000 --- a/changelog/472.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Added `fiasco.IonCollection.build_line_list` for building a table of the emission lines of every ion in a collection, optionally restricted to a wavelength range and a minimum abundance, with the contribution function of each line and, if a DEM model is given, its intensity. diff --git a/fiasco/collections.py b/fiasco/collections.py index cc57c089..9aeacdcf 100644 --- a/fiasco/collections.py +++ b/fiasco/collections.py @@ -285,12 +285,16 @@ def build_line_list(self, self.log.warning(f'{ion.ion_name} not included in line list. {e}') continue if abundance < minimum_abundance: + self.log.info(f'{ion.ion_name} not included in line list. ' + f'Abundance {abundance:.3g} is below minimum_abundance={minimum_abundance:.3g}.') continue is_bound_bound = transitions.is_bound_bound wavelength = transitions.wavelength[is_bound_bound] in_range = np.logical_and(wavelength >= wavelength_range[0], wavelength <= wavelength_range[1]) if not in_range.any(): + self.log.info(f'{ion.ion_name} not included in line list. ' + f'No transitions between {wavelength_range[0]:.3f} and {wavelength_range[1]:.3f}.') continue try: g = ion.contribution_function(density, **kwargs) @@ -329,8 +333,10 @@ def build_line_list(self, line_list = vstack(tables) line_list.meta['temperature'] = self.temperature line_list.meta['density'] = density + # Each ion in a collection can use its own abundance and ionization fraction + # datasets, so record the names from the ions that made it into the table. + # The name is None for an ion whose dataset was set to a value directly. for key in ('abundance', 'ionization_fraction'): - # the name is None for an ion whose dataset was set to a value directly names = sorted({ion._dset_names[key] for ion in included}, key=str) line_list.meta[key] = names[0] if len(names) == 1 else names line_list.meta['chianti_version'] = str(included[0]._elvlc.version) diff --git a/fiasco/tests/test_collections.py b/fiasco/tests/test_collections.py index 3b69b74f..0465cdee 100644 --- a/fiasco/tests/test_collections.py +++ b/fiasco/tests/test_collections.py @@ -2,6 +2,7 @@ Test collection functionality """ import astropy.units as u +import logging import numpy as np import pytest @@ -257,11 +258,23 @@ def test_build_line_list_meta(line_list, line_list_collection): @pytest.mark.requires_dbase_version('>= 8') def test_build_line_list_minimum_abundance(line_list_collection): - # Selecting by abundance removes iron + # Selecting by abundance removes iron. H II passes the abundance cut but has no + # transitions, so it is never in the line list. line_list = line_list_collection.build_line_list(line_list_density, minimum_abundance=1e-3) assert set(line_list['ion_name']) == {'H I'} +@pytest.mark.requires_dbase_version('>= 8') +def test_build_line_list_skipped_ions_logged(line_list_collection, caplog): + with caplog.at_level(logging.INFO, logger='fiasco'): + line_list_collection.build_line_list(line_list_density, minimum_abundance=1e-3) + assert 'Fe 5 not included in line list. Abundance' in caplog.text + caplog.clear() + with caplog.at_level(logging.INFO, logger='fiasco'): + line_list_collection.build_line_list(line_list_density, wavelength_range=[1215, 1216] * u.angstrom) + assert 'Fe 5 not included in line list. No transitions between' in caplog.text + + @pytest.mark.requires_dbase_version('>= 8') def test_build_line_list_wavelength_range(line_list_collection): wavelength_range = [900, 1300] * u.angstrom From 9ec54f9929ae8e854d1a45a5f970ad81fd82a28e Mon Sep 17 00:00:00 2001 From: Will Barnes Date: Fri, 23 Oct 2026 16:21:44 -0400 Subject: [PATCH 04/12] Update fiasco/collections.py --- fiasco/collections.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/fiasco/collections.py b/fiasco/collections.py index 9aeacdcf..1dfaf2e0 100644 --- a/fiasco/collections.py +++ b/fiasco/collections.py @@ -307,19 +307,19 @@ def build_line_list(self, n_lines = in_range.sum() table = QTable({ 'element': [ion.atomic_symbol] * n_lines, - 'atomic_number': [ion.atomic_number] * n_lines, - 'ion_name': [ion.ion_name_roman] * n_lines, + 'atomic number': [ion.atomic_number] * n_lines, + 'ion name': [ion.ion_name_roman] * n_lines, 'abundance': [abundance] * n_lines, - 'ionization_potential': u.Quantity([ion.ionization_potential] * n_lines), + 'ionization potential': u.Quantity([ion.ionization_potential] * n_lines), 'wavelength': wavelength[in_range], 'energy': transitions.delta_energy[is_bound_bound][in_range], 'A': transitions.A[is_bound_bound][in_range], - 'upper_level': transitions.upper_level[is_bound_bound][in_range], - 'lower_level': transitions.lower_level[is_bound_bound][in_range], - 'upper_label': transitions.upper_label[is_bound_bound][in_range], - 'lower_label': transitions.lower_label[is_bound_bound][in_range], - 'is_observed': transitions.is_observed[is_bound_bound][in_range], - 'contribution_function': g, + 'upper level': transitions.upper_level[is_bound_bound][in_range], + 'lower level': transitions.lower_level[is_bound_bound][in_range], + 'upper label': transitions.upper_label[is_bound_bound][in_range], + 'lower label': transitions.lower_label[is_bound_bound][in_range], + 'is observed': transitions.is_observed[is_bound_bound][in_range], + 'contribution function': g, }) if emission_measure is not None: f_interp = interp1d(log_temperature, g.value, axis=1, bounds_error=False, fill_value=0.0) From 1f93e9f7241be490342f2ba7da66f0a099eb63a7 Mon Sep 17 00:00:00 2001 From: Will Barnes Date: Fri, 23 Oct 2026 16:21:58 -0400 Subject: [PATCH 05/12] Update fiasco/tests/test_collections.py --- fiasco/tests/test_collections.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fiasco/tests/test_collections.py b/fiasco/tests/test_collections.py index 0465cdee..b1f8624b 100644 --- a/fiasco/tests/test_collections.py +++ b/fiasco/tests/test_collections.py @@ -222,7 +222,7 @@ def test_build_line_list_rows(line_list, line_list_collection): n_h1 = h1.transitions.is_bound_bound.sum() n_fe5 = fe5.transitions.is_bound_bound.sum() assert len(line_list) == n_h1 + n_fe5 - assert set(line_list['ion_name']) == {'H I', 'Fe V'} + assert set(line_list['ion name']) == {'H I', 'Fe V'} assert 'intensity' not in line_list.colnames From 82233d5063a00f87f71fba864676adfdff787905 Mon Sep 17 00:00:00 2001 From: Will Barnes Date: Fri, 23 Oct 2026 16:22:08 -0400 Subject: [PATCH 06/12] Update fiasco/tests/test_collections.py --- fiasco/tests/test_collections.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fiasco/tests/test_collections.py b/fiasco/tests/test_collections.py index b1f8624b..6e0abf78 100644 --- a/fiasco/tests/test_collections.py +++ b/fiasco/tests/test_collections.py @@ -229,7 +229,7 @@ def test_build_line_list_rows(line_list, line_list_collection): @pytest.mark.requires_dbase_version('>= 8') def test_build_line_list_levels(line_list, line_list_collection): _, fe5, _ = line_list_collection - is_fe5 = line_list['ion_name'] == 'Fe V' + is_fe5 = line_list['ion name'] == 'Fe V' assert np.array_equal(line_list['upper_level'][is_fe5], fe5.transitions.upper_level[fe5.transitions.is_bound_bound]) assert np.all(line_list['upper_level'] > line_list['lower_level']) From 95e9244fa4d15afb5916aa59f23a80c1db86b3dc Mon Sep 17 00:00:00 2001 From: Will Barnes Date: Fri, 23 Oct 2026 16:22:15 -0400 Subject: [PATCH 07/12] Update fiasco/tests/test_collections.py --- fiasco/tests/test_collections.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fiasco/tests/test_collections.py b/fiasco/tests/test_collections.py index 6e0abf78..d02fbc98 100644 --- a/fiasco/tests/test_collections.py +++ b/fiasco/tests/test_collections.py @@ -230,7 +230,7 @@ def test_build_line_list_rows(line_list, line_list_collection): def test_build_line_list_levels(line_list, line_list_collection): _, fe5, _ = line_list_collection is_fe5 = line_list['ion name'] == 'Fe V' - assert np.array_equal(line_list['upper_level'][is_fe5], + assert np.array_equal(line_list['upper level'][is_fe5], fe5.transitions.upper_level[fe5.transitions.is_bound_bound]) assert np.all(line_list['upper_level'] > line_list['lower_level']) assert np.all(line_list['energy'] > 0 * u.eV) From 95dd10405ef4322fc67cf5207192fc9015e0bbfd Mon Sep 17 00:00:00 2001 From: Will Barnes Date: Fri, 23 Oct 2026 16:22:28 -0400 Subject: [PATCH 08/12] Update fiasco/tests/test_collections.py --- fiasco/tests/test_collections.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fiasco/tests/test_collections.py b/fiasco/tests/test_collections.py index d02fbc98..54f2f86b 100644 --- a/fiasco/tests/test_collections.py +++ b/fiasco/tests/test_collections.py @@ -232,7 +232,7 @@ def test_build_line_list_levels(line_list, line_list_collection): is_fe5 = line_list['ion name'] == 'Fe V' assert np.array_equal(line_list['upper level'][is_fe5], fe5.transitions.upper_level[fe5.transitions.is_bound_bound]) - assert np.all(line_list['upper_level'] > line_list['lower_level']) + assert np.all(line_list['upper level'] > line_list['lower level']) assert np.all(line_list['energy'] > 0 * u.eV) From 88c269295372a193bfb8409f64ae6b65a7d078d4 Mon Sep 17 00:00:00 2001 From: Will Barnes Date: Fri, 23 Oct 2026 16:22:35 -0400 Subject: [PATCH 09/12] Update fiasco/tests/test_collections.py --- fiasco/tests/test_collections.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fiasco/tests/test_collections.py b/fiasco/tests/test_collections.py index 54f2f86b..395254a1 100644 --- a/fiasco/tests/test_collections.py +++ b/fiasco/tests/test_collections.py @@ -239,7 +239,7 @@ def test_build_line_list_levels(line_list, line_list_collection): @pytest.mark.requires_dbase_version('>= 8') def test_build_line_list_contribution_function(line_list, line_list_collection): _, fe5, _ = line_list_collection - assert line_list['contribution_function'].shape == (len(line_list), temperature.shape[0], 1) + assert line_list['contribution function'].shape == (len(line_list), temperature.shape[0], 1) g_fe5 = fe5.contribution_function(line_list_density) assert u.allclose(line_list['contribution_function'][line_list['ion_name'] == 'Fe V'], np.moveaxis(g_fe5, -1, 0)) From aaeb1cf48bac88cdb4ec4a84bc0e47e80365b9b7 Mon Sep 17 00:00:00 2001 From: Will Barnes Date: Fri, 23 Oct 2026 16:22:43 -0400 Subject: [PATCH 10/12] Update fiasco/tests/test_collections.py --- fiasco/tests/test_collections.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fiasco/tests/test_collections.py b/fiasco/tests/test_collections.py index 395254a1..a6f06061 100644 --- a/fiasco/tests/test_collections.py +++ b/fiasco/tests/test_collections.py @@ -241,7 +241,7 @@ def test_build_line_list_contribution_function(line_list, line_list_collection): _, fe5, _ = line_list_collection assert line_list['contribution function'].shape == (len(line_list), temperature.shape[0], 1) g_fe5 = fe5.contribution_function(line_list_density) - assert u.allclose(line_list['contribution_function'][line_list['ion_name'] == 'Fe V'], + assert u.allclose(line_list['contribution function'][line_list['ion name'] == 'Fe V'], np.moveaxis(g_fe5, -1, 0)) From f06040f2fa9ff2ce9830a548f9b150770376521f Mon Sep 17 00:00:00 2001 From: Will Barnes Date: Fri, 23 Oct 2026 16:22:51 -0400 Subject: [PATCH 11/12] Update fiasco/tests/test_collections.py --- fiasco/tests/test_collections.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fiasco/tests/test_collections.py b/fiasco/tests/test_collections.py index a6f06061..49b606b4 100644 --- a/fiasco/tests/test_collections.py +++ b/fiasco/tests/test_collections.py @@ -261,7 +261,7 @@ def test_build_line_list_minimum_abundance(line_list_collection): # Selecting by abundance removes iron. H II passes the abundance cut but has no # transitions, so it is never in the line list. line_list = line_list_collection.build_line_list(line_list_density, minimum_abundance=1e-3) - assert set(line_list['ion_name']) == {'H I'} + assert set(line_list['ion name']) == {'H I'} @pytest.mark.requires_dbase_version('>= 8') From 2b416c73c66d495930d6c2d8f29cefcd495f1789 Mon Sep 17 00:00:00 2001 From: Will Barnes Date: Fri, 23 Oct 2026 16:22:59 -0400 Subject: [PATCH 12/12] Update fiasco/tests/test_collections.py --- fiasco/tests/test_collections.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fiasco/tests/test_collections.py b/fiasco/tests/test_collections.py index 49b606b4..2d4e30e6 100644 --- a/fiasco/tests/test_collections.py +++ b/fiasco/tests/test_collections.py @@ -281,7 +281,7 @@ def test_build_line_list_wavelength_range(line_list_collection): line_list = line_list_collection.build_line_list(line_list_density, wavelength_range=wavelength_range) assert np.all(line_list['wavelength'] >= wavelength_range[0]) assert np.all(line_list['wavelength'] <= wavelength_range[1]) - assert 'H I' in set(line_list['ion_name']) + assert 'H I' in set(line_list['ion name']) @pytest.mark.requires_dbase_version('>= 8')