Repository navigation
Add IonCollection.build_line_list - #472
jacobdparker wants to merge 2 commits into
Conversation
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 wtbarnes#464 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
For context on the consumer side: I have rewritten the MUSE pipeline's |
| # Selecting by abundance removes iron | ||
| line_list = c.build_line_list(density, minimum_abundance=1e-3) | ||
| assert set(line_list['ion_name']) == {'H I'} |
There was a problem hiding this comment.
Done, now test_build_line_list_minimum_abundance.
| # 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']) |
There was a problem hiding this comment.
Done, now test_build_line_list_wavelength_range.
| 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)) |
There was a problem hiding this comment.
I'd prefer this be broken up into 2 or 3 tests which all take in a single line list fixture. That way, if something breaks, it's easier to see what has broken. As written, this test could fail for any number of reasons and not be very informative.
There was a problem hiding this comment.
Done in cfdeb3c: one line_list fixture (plus the collection it is built from) and separate tests for the rows, the level indices, the contribution function, the meta, minimum_abundance, and wavelength_range.
| couple_density_to_temperature: `bool`, optional | ||
| If True, the density will vary along the same axis as temperature. | ||
| See `fiasco.Ion.contribution_function`. |
There was a problem hiding this comment.
It's odd that this kwarg is explicitly documented while as written, this function allows you to pass in any of the keyword arguments that propagate down to contribution_function. I suggest either keeping this documentation and only allowing this kwarg to propagate down or removing this and just adding a note that says any kwargs that can be passed to contribution_function can also be passed here.
There was a problem hiding this comment.
Done: replaced with a kwargs entry saying that any keyword argument accepted by contribution_function is passed through, with couple_density_to_temperature as the example.
| 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. |
There was a problem hiding this comment.
The explanation of the contribution function shape is helpful, but it isn't necessary to document every column.
There was a problem hiding this comment.
Done: dropped the column list and kept the shape explanation.
| try: | ||
| ionization_potential = ion.ionization_potential | ||
| except MissingDatasetException: | ||
| ionization_potential = np.nan * u.eV |
There was a problem hiding this comment.
We don't need to guard against this. There should not be cases where an ionization potential is missing.
There was a problem hiding this comment.
Done, removed.
| '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()), |
There was a problem hiding this comment.
It is better to use transitions.energy[is_bound_bound][in_range] as these are the energies directly from the database.
There was a problem hiding this comment.
Done, using transitions.delta_energy[is_bound_bound][in_range]. There is no transitions.energy; delta_energy is the upper minus lower level energy from the elvlc data, which I take to be what you meant. For observed lines it agrees with the value derived from the wavelength, as expected.
| line_list.meta['temperature'] = self.temperature | ||
| line_list.meta['density'] = density |
There was a problem hiding this comment.
Should also add the abundance and ioneq dataset names, CHIANTI dbase version and fiasco version.
There was a problem hiding this comment.
Done: meta now also has abundance and ionization_fraction (the dataset names, a single string when every ion shares one and otherwise the sorted list), chianti_version from _elvlc.version of the first ion in the list, and fiasco_version.
- 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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SPbZHPLHsKKTtmMueYx5RM
Closes #464
Adds
IonCollection.build_line_list(density, minimum_abundance=0, wavelength_range=None, emission_measure=None, **kwargs), following the mock-up in #464. It loops over the ions in the collection, skips those below the abundance cutoff or with no bound-bound transition in the wavelength range (and, with a warning, those missing the data to compute a contribution function), and returns aQTablewith one row per transition.Columns:
element,atomic_number,ion_name(roman),abundance,ionization_potential,wavelength,energy,A,upper_level,lower_level,upper_label,lower_label,is_observed,contribution_function, andintensitywhen a DEM is given. Thecontribution_functionentry of each row has shape(l, m)over temperature and density, so a row is one line and its G(n, T).kwargspass through tocontribution_function, socouple_density_to_temperatureworks as usual.Two choices worth a look:
table.metarather than as columns, since they are the same for every row. Easy to change if you would rather have them as columns.emission_measuretakes the table returned byget_dem_model. The intensity is the sum over the model's temperature bins ofemtimes the contribution function interpolated (in log T) to the bin centers, divided by 4π sr. Bins outside the temperature range of the collection contribute nothing.Example, seven ions over 550–680 Å with the quiet-Sun DEM at constant pressure, brightest first:
I also checked it against the line-list builder in the MUSE pipeline that #464 links to, run on the same fiasco checkout: four ions over 170–185 Å at two pressures. Same lines in the same order and bit-identical G(n, T), apart from one theoretical Fe X line at exactly 185.0 Å which the inclusive bounds here keep and MUSE's strict bounds drop.
Tests cover the row count and contribution function against
Ion.contribution_function, the abundance and wavelength filters, the intensity column with a DEM model, and the error when no ion has lines. The fulltest_collections.pymodule passes against the full CHIANTI database.🤖 Generated with Claude Code