Skip to content

Add IonCollection.build_line_list - #472

Open
jacobdparker wants to merge 2 commits into
wtbarnes:mainfrom
jacobdparker:line-list
Open

jacobdparker wants to merge 2 commits into
wtbarnes:mainfrom
jacobdparker:line-list

Conversation

@jacobdparker

Copy link
Copy Markdown
Collaborator

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 a QTable with 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, and intensity when a DEM is given. The contribution_function entry of each row has shape (l, m) over temperature and density, so a row is one line and its G(n, T). kwargs pass through to contribution_function, so couple_density_to_temperature works as usual.

Two choices worth a look:

  • The temperature and density are stored in table.meta rather than as columns, since they are the same for every row. Easy to change if you would rather have them as columns.
  • emission_measure takes the table returned by get_dem_model. The intensity is the sum over the model's temperature bins of em times 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:

ion_name wavelength upper_label  lower_label  is_observed    intensity
          Angstrom                                        erg / (s sr cm2)
-------- ---------- ------------ ------------ ----------- ----------------
     O V    629.732    2s 2p 1P1      2s2 1S0        True              418
    He I    584.334    1s 2p 1P1      1s2 1S0        True              196
    O IV    554.514 2s 2p2 2P3/2 2s2 2p 2P3/2        True              142
    Mg X    609.793 1s2 2p 2P3/2 1s2 2s 2S1/2        True              104
    O IV    554.076 2s 2p2 2P1/2 2s2 2p 2P1/2        True             55.2
    Mg X    624.941 1s2 2p 2P1/2 1s2 2s 2S1/2        True             51.3
   O III    599.590   2s 2p3 1D2  2s2 2p2 1D2        True             51.2
    O IV    609.830 2s 2p2 2S1/2 2s2 2p 2P3/2        True             44.6

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 full test_collections.py module passes against the full CHIANTI database.

🤖 Generated with Claude Code

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>
@jacobdparker

Copy link
Copy Markdown
Collaborator Author

For context on the consumer side: I have rewritten the MUSE pipeline's fiasco_gofnt_linelist on top of this method, on the branch feature/fiasco-line-list in the MUSE repo (https://gitlab.com/LMSAL_HUB/muse_hub/muse/-/tree/feature/fiasco-line-list, commit e93d9bbf). That function is now just grid preparation, building the IonCollection, and reshaping the table into the xarray layout the pipeline expects. On the same fiasco checkout it reproduces the previous MUSE implementation exactly: same lines in the same order and bit-identical G(n, T) for four ions over 170–185 Å at two pressures. The only fields MUSE needed beyond the mock-up in #464 were the level indices, which is why upper_level and lower_level are in the table.

Comment on lines +226 to +228
# Selecting by abundance removes iron
line_list = c.build_line_list(density, minimum_abundance=1e-3)
assert set(line_list['ion_name']) == {'H I'}

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be a separate test

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, now test_build_line_list_minimum_abundance.

Comment thread fiasco/tests/test_collections.py Outdated
Comment on lines +229 to +234
# 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'])

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be a separate test.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, now test_build_line_list_wavelength_range.

Comment on lines +212 to +225
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))

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread fiasco/collections.py Outdated
Comment on lines +251 to +253
couple_density_to_temperature: `bool`, optional
If True, the density will vary along the same axis as temperature.
See `fiasco.Ion.contribution_function`.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread fiasco/collections.py Outdated
Comment on lines +258 to +267
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.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The explanation of the contribution function shape is helpful, but it isn't necessary to document every column.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done: dropped the column list and kept the shape explanation.

Comment thread fiasco/collections.py Outdated
Comment on lines +302 to +305
try:
ionization_potential = ion.ionization_potential
except MissingDatasetException:
ionization_potential = np.nan * u.eV

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need to guard against this. There should not be cases where an ionization potential is missing.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, removed.

Comment thread fiasco/collections.py Outdated
'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()),

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is better to use transitions.energy[is_bound_bound][in_range] as these are the energies directly from the database.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread fiasco/collections.py
Comment on lines +335 to +336
line_list.meta['temperature'] = self.temperature
line_list.meta['density'] = density

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should also add the abundance and ioneq dataset names, CHIANTI dbase version and fiasco version.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add convenience method to IonCollection for generating a line list

2 participants