Skip to content

feat(exomol): support multiple diluent broadening partners (fixes #725) - #966

Open
MJO7 wants to merge 3 commits into
radis:developfrom
MJO7:feat/exomol-multi-diluent-broadening
Open

MJO7 wants to merge 3 commits into
radis:developfrom
MJO7:feat/exomol-multi-diluent-broadening

Conversation

@MJO7

@MJO7 MJO7 commented Mar 12, 2026 •

Copy link
Copy Markdown

This PR addresses issue #725 — ExoMol-based spectra couldn't use multiple broadening partners the way HITRAN/HITEMP already could (via PR #555). If you were simulating a H2/He-dominated atmosphere, you'd either hit a NotImplementedError or silently fall back to air broadening with no warning. I tried to implement the following changes.

exomolapi.py — There was a small existing bug where H2 and NH3 both appeared twice in broad_partners (deduplicated that). More importantly, set_broadening_coef had a hard raise NotImplementedError for any species that wasn't air or self. Replaced that with the actual logic: write gamma_ / n_ columns (e.g. gamma_h2, n_h2), which radis.lbl.broadening._calc_broadening_HWHM already handles generically — no changes needed there. Also wired in the MISSING_BROAD_COEF config flag (from PR #677) so missing .broad files give a clear ValueError with actionable suggestions rather than a silent fallback.

exomol.py — Added a diluent parameter to fetch_exomol() and loop over requested species calling set_broadening_coef for each. Air and self are skipped since they're already handled upstream.

calc.py — Passed diluent through into the ExoMol conditions dict and removed the guard blocking non-air diluents from reaching the ExoMol path.

loader.py — Docstring addition so the diluent kwarg is documented for ExoMol users in fetch_databank.

Tests — 6 @pytest.mark.fast unit tests (no network) covering column naming, air/self backward compatibility, both MISSING_BROAD_COEF policy branches, and unknown-species warnings. Plus 2 @pytest.mark.needs_connection integration tests.

One gap worth flagging: the GPU path in factory.py still hardcodes selbrd/airbrd, so non-air diluents are silently ignored in GPU mode.

…is#602)

Extend ExoMol spectral calculations to accept arbitrary broadening
partners (H2, He, CO2, N2, …) via a `diluent=` dict, matching the
behaviour already available for HITRAN/HITEMP sources (PR radis#555).

Changes:
- exomolapi.py: deduplicate `broad_partners` list (H2/NH3 appeared
  twice); extend `set_broadening_coef` to write `gamma_<species>` /
  `n_<species>` columns for non-air/self species; honour the
  `MISSING_BROAD_COEF` config flag (raise ValueError or emit
  MissingDiluentBroadeningWarning) instead of NotImplementedError
- exomol.py: add `diluent` parameter to `fetch_exomol`; loop over
  requested species and call `set_broadening_coef` for each
- calc.py: pass `diluent` into ExoMol conditions dict; remove the
  guard that raised NotImplementedError for non-air diluents
- loader.py: document the `diluent` kwarg in `fetch_databank` docstring
- tests: 8 new @pytest.mark.fast unit tests (no network) covering
  column naming, backward compat, MISSING_BROAD_COEF policy, and
  unknown-species warning; 2 new @pytest.mark.needs_connection
  integration tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 12, 2026 06:27

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds ExoMol parity with HITRAN/HITEMP for multi-diluent pressure broadening by enabling partner-specific broadening columns and propagating diluent through the ExoMol fetch/calc path, with tests and updated docs.

Changes:

  • Extend ExoMol broadening support to write gamma_<partner> / n_<partner> columns for non-air/non-self partners and respect radis.config["MISSING_BROAD_COEF"].
  • Propagate diluent into the ExoMol fetch pipeline and remove the ExoMol “air-only” guard in calc.
  • Add/extend unit + integration tests and document the diluent kwarg for ExoMol in fetch_databank.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
radis/api/exomolapi.py Deduplicates broad_partners; implements non-air/non-self broadening column creation + missing-broadening policy handling.
radis/io/exomol.py Adds diluent parameter and loops over requested partners to load partner-specific broadening coefficients.
radis/lbl/calc.py Removes ExoMol non-air diluent guard and passes diluent into ExoMol fetch conditions.
radis/lbl/loader.py Documents diluent for ExoMol users and clarifies interaction with MISSING_BROAD_COEF.
radis/test/api/test_exomolapi.py Adds fast unit tests for ExoMol broadening columns, missing-file policy, and unknown partner warnings.
radis/test/io/test_exomol.py Adds connection tests exercising multi-diluent broadening and missing-broad file behavior end-to-end.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +173 to +193
@pytest.mark.needs_connection
def test_exomol_diluent_error_when_missing_broad_file(verbose=True, *args, **kwargs):
"""When a requested diluent species has no .broad file and
MISSING_BROAD_COEF is False (default), a ValueError must be raised.

Uses a fictitious species 'ZZZ' that will never have a .broad file.
"""
import radis
from radis import SpectrumFactory

radis.config["MISSING_BROAD_COEF"] = False
try:
sf = SpectrumFactory(**conditions)
with pytest.raises((ValueError, KeyError)):
sf.fetch_databank(
source="exomol",
broadf=True,
broadf_download=False,
diluent={"ZZZ": 0.9},
)
finally:

Copilot AI Mar 12, 2026

Copy link

Choose a reason for hiding this comment

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

test_exomol_diluent_error_when_missing_broad_file is expecting an exception during fetch_databank(...), but fetch_exomol() skips unknown partners (like 'ZZZ') with a warning and does not attempt to load a .broad file. The error for an unsupported diluent would occur later during spectrum calculation (broadening) or not at all if it is skipped. To make this test meaningful, either: (1) assert the warning for unknown species, or (2) use a known ExoMol partner (e.g. 'H2'/'He') with a missing .broad file and assert the ValueError path, and/or trigger the failure via eq_spectrum(...) where broadening is computed.

Copilot uses AI. Check for mistakes.
Comment on lines +75 to +102
@pytest.mark.fast
def test_broad_partners_no_duplicates():
"""The hardcoded ExoMol broadening partner list must not contain duplicates.

Regression test: H2 and NH3 appeared twice before the fix.
See https://github.com/radis/radis/issues/602
"""
# We inspect the list built during __init__; use dict.fromkeys logic
raw_partners = [
"H2",
"He",
"air",
"self",
"Ar",
"CH4",
"CO",
"CO2",
"H2O",
"N2",
"NH3",
"NO",
"O2",
"CS",
]
deduped = list(dict.fromkeys(raw_partners))
assert len(deduped) == len(set(deduped)), "broad_partners contains duplicates"
assert deduped == raw_partners, "broad_partners order changed unexpectedly"

Copilot AI Mar 12, 2026

Copy link

Choose a reason for hiding this comment

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

test_broad_partners_no_duplicates currently only deduplicates a hard-coded local raw_partners list and never inspects MdbExomol.broad_partners as constructed by the production code. This means the test will still pass even if duplicates are reintroduced in MdbExomol.__init__. Update the test to derive the partner list from the actual implementation (e.g., instantiate MdbExomol with a temporary path and mocked metadata/network, or import/inspect the list/constant used by __init__) so it can catch regressions.

Copilot uses AI. Check for mistakes.
Comment on lines +253 to +271
mock_instance.rename_columns.return_value = None
mock_instance.add_column = lambda df, name, vals: None
mock_instance.load.return_value = dummy_df
mock_instance.to_partition_function_tabulator.return_value = None

with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
try:
fetch_exomol(
"CO",
database="HITEMP-CO",
local_databases="/tmp/fake",
diluent={"XYZ": 0.5, "H2": 0.5},
output="pandas",
verbose=False,
cache="force",
)
except Exception:
pass # we only care about the warning, not the full output

Copilot AI Mar 12, 2026

Copy link

Choose a reason for hiding this comment

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

test_fetch_exomol_diluent_unknown_species_warns wraps fetch_exomol(...) in a broad try/except Exception: pass, but with the current mocking it will reliably raise later (e.g. fetch_exomol asserts 'wav' in df after rename_columns, while the mock rename_columns is a no-op). This makes the test unable to verify the docstring claim that the function “does not raise”. Prefer to mock/patch enough so that fetch_exomol(...) completes successfully (e.g. make rename_columns actually rename nu_lines -> wav on dummy_df), and then remove the exception swallowing so a real regression can’t be hidden.

Suggested change
mock_instance.rename_columns.return_value = None
mock_instance.add_column = lambda df, name, vals: None
mock_instance.load.return_value = dummy_df
mock_instance.to_partition_function_tabulator.return_value = None
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
try:
fetch_exomol(
"CO",
database="HITEMP-CO",
local_databases="/tmp/fake",
diluent={"XYZ": 0.5, "H2": 0.5},
output="pandas",
verbose=False,
cache="force",
)
except Exception:
pass # we only care about the warning, not the full output
mock_instance.rename_columns.side_effect = lambda df: df.rename(
columns={"nu_lines": "wav"}, inplace=True
) or df
mock_instance.add_column = lambda df, name, vals: None
mock_instance.load.return_value = dummy_df
mock_instance.to_partition_function_tabulator.return_value = None
with warnings.catch_warnings(record=True) as caught:
warnings.simplefilter("always")
fetch_exomol(
"CO",
database="HITEMP-CO",
local_databases="/tmp/fake",
diluent={"XYZ": 0.5, "H2": 0.5},
output="pandas",
verbose=False,
cache="force",
)

Copilot uses AI. Check for mistakes.
Comment thread radis/api/exomolapi.py
Comment on lines +1502 to +1506
_broad_file_loaded = False # tracks whether a .broad file was successfully read

if self.broadf and os.path.exists(file):
bdat = read_broad(file, output)
_broad_file_loaded = True

Copilot AI Mar 12, 2026

Copy link

Choose a reason for hiding this comment

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

_broad_file_loaded is set but never used. This looks like leftover instrumentation and adds noise to a relatively complex method; please remove it or use it (e.g. to guard later logic / improve logging) so it doesn’t become misleading dead state.

Suggested change
_broad_file_loaded = False # tracks whether a .broad file was successfully read
if self.broadf and os.path.exists(file):
bdat = read_broad(file, output)
_broad_file_loaded = True
if self.broadf and os.path.exists(file):
bdat = read_broad(file, output)

Copilot uses AI. Check for mistakes.
Comment thread radis/api/exomolapi.py
Comment on lines +1671 to +1686
if not config["MISSING_BROAD_COEF"]:
raise ValueError(
f"{msg_missing}{list_solutions}"
)
else:
# config["MISSING_BROAD_COEF"] == "air": warn and skip column
from radis.misc.warning import MissingDiluentBroadeningWarning

warnings.warn(
f"{msg_missing}\n"
f"radis.config['MISSING_BROAD_COEF'] = 'air' is set: "
f"air broadening will be used instead of {species}.\n"
"You can silence this warning with "
"`warnings['MissingDiluentBroadeningWarning'] = 'ignore'`.",
MissingDiluentBroadeningWarning,
)

Copilot AI Mar 12, 2026

Copy link

Choose a reason for hiding this comment

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

The missing-broadening branch treats any truthy radis.config['MISSING_BROAD_COEF'] as the 'air' policy, and the warning text hardcodes that 'air' is set. However, the config is documented/validated elsewhere to only allow False or 'air'. Consider validating the config value here as well (and/or checking explicitly for 'air') so misconfiguration doesn’t produce a misleading message or unexpected behavior.

Copilot uses AI. Check for mistakes.
Comment thread radis/io/exomol.py
Comment on lines +311 to +312
"No `.broad` file will be loaded for this species; "
"broadening will fall back to air coefficients.",

Copilot AI Mar 12, 2026

Copy link

Choose a reason for hiding this comment

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

The warning for an unknown ExoMol broadening partner says “broadening will fall back to air coefficients”, but fetch_exomol() only skips loading the .broad file and does not remove the species from the diluent dict used later by radis.lbl.broadening. With the default radis.config['MISSING_BROAD_COEF']=False, this will still raise a ValueError during spectrum calculation because gamma_<species> is missing. Either adjust the message to reflect the actual behavior (depends on MISSING_BROAD_COEF), or proactively drop/normalize unknown diluent keys so the fallback is truly automatic.

Suggested change
"No `.broad` file will be loaded for this species; "
"broadening will fall back to air coefficients.",
"No `.broad` file will be loaded for this species; subsequent "
"handling of missing broadening coefficients depends on the "
"RADIS configuration and line-broadening implementation.",

Copilot uses AI. Check for mistakes.
Comment on lines +117 to +171
# Allow graceful fall-back to air when a .broad file is missing on the
# ExoMol server (some molecules only have H2 but not He, etc.)
radis.config["MISSING_BROAD_COEF"] = "air"

try:
from radis import SpectrumFactory

sf_air = SpectrumFactory(**conditions)
sf_air.fetch_databank(
source="exomol",
broadf=True,
broadf_download=True,
)
s_air = sf_air.eq_spectrum(Tgas=1000, path_length=1)

# H2-dominated atmosphere: mole fractions must sum to 1 with the molecule.
# conditions has mole_fraction=0.1 for CO, so diluents must sum to 0.9.
h2_he_diluent = {"H2": 0.765, "He": 0.135} # 0.1 + 0.765 + 0.135 = 1.0

sf_h2 = SpectrumFactory(**conditions)
sf_h2.fetch_databank(
source="exomol",
broadf=True,
broadf_download=True,
diluent=h2_he_diluent,
)
s_h2 = sf_h2.eq_spectrum(
Tgas=1000,
path_length=1,
diluent=h2_he_diluent,
)

# Line integrals (abscoeff) should be the same regardless of broadening
# partner (broadening redistributes but doesn't create/destroy absorption)
assert np.isclose(
s_air.get_integral("abscoeff"),
s_h2.get_integral("abscoeff"),
rtol=0.005,
), (
"Integral of abscoeff differs between air and H2/He broadening. "
"Broadening should not change the integrated line strength."
)

# The line shapes will differ: check hwhm_lorentz in df1
wl_air = sf_air.df1["hwhm_lorentz"].values
wl_h2 = sf_h2.df1["hwhm_lorentz"].values
# They should NOT be identical (different broadening partners)
assert not np.allclose(wl_air, wl_h2, rtol=1e-4), (
"Lorentzian HWHM is the same for air and H2/He broadening – "
"the diluent is not being applied."
)

finally:
radis.config["MISSING_BROAD_COEF"] = False # restore default

Copilot AI Mar 12, 2026

Copy link

Choose a reason for hiding this comment

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

Tests mutate radis.config['MISSING_BROAD_COEF'] but restore it to False unconditionally. This can leak state across the test suite when the config was set differently before the test (e.g. via another test or user env). Save the original value at the start of the test and restore that in finally (or use monkeypatch).

Copilot uses AI. Check for mistakes.
@MJO7 MJO7 changed the title feat(exomol): support multiple diluent broadening partners (fixes #602) feat(exomol): support multiple diluent broadening partners (fixes #725) Mar 12, 2026
@MJO7

MJO7 commented Mar 13, 2026

Copy link
Copy Markdown
Author

Hello! The RTD build failure is being caused by np.trapz being removed in NumPy 2.x (see examples/2_Experimental_spectra/plot_chain_spectrum_edition.py:117). This same issue likely affects the base branch with Python 3.14. The PR code itself has no documentation or import issues that relate to this error. How should I proceed with this?

…2.x)

np.trapz was removed in NumPy 2.0; use np.trapezoid instead to fix
RTD docs build failure.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@codecov

codecov Bot commented Apr 22, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.33333% with 11 lines in your changes missing coverage. Please review.
✅ Project coverage is 69.75%. Comparing base (b4248fc) to head (583e6cd).
⚠️ Report is 1299 commits behind head on develop.

Additional details and impacted files
@@             Coverage Diff             @@
##           develop     #966      +/-   ##
===========================================
- Coverage    73.23%   69.75%   -3.48%     
===========================================
  Files          146      150       +4     
  Lines        21793    23151    +1358     
===========================================
+ Hits         15960    16149     +189     
- Misses        5833     7002    +1169     
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@minouHub minouHub left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Sorry for missing this PR. There were too many to review. This looks good and can be merged with the minor ajustements I suggested.

Also glad to see someone from UCLA and rocket project!

# ---------------------------------------------------------------------------


def _make_mock_mdb(broad_files, broadf=True, engine="pandas"):

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

We already download the broadening coefficients for CO in test_calc_exomol_vs_hitemp, see test_exomol.py. So I'm not sure building a mock database is necessary

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Please rebase or merge develop (this issue was solved)

from radis import SpectrumFactory

radis.config["MISSING_BROAD_COEF"] = False
try:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Remove the try instance. Simply execute your code

# ExoMol server (some molecules only have H2 but not He, etc.)
radis.config["MISSING_BROAD_COEF"] = "air"

try:

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

No try except here. Simply execute your code

Comment thread radis/io/exomol.py
if species not in mdb.broad_partners:
import warnings

warnings.warn(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This should check the value of radis.config["MISSING_BROAD_COEF"]. If this is False, you should raise an Error, not a Warning. This is why your test test_exomol_diluent_error_when_missing_broad_file is failing.

)

finally:
radis.config["MISSING_BROAD_COEF"] = False # restore default

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

You can move this line before the assert to make sure it is executed

@minouHub minouHub added this to the 0.17.1 milestone Apr 22, 2026
@minouHub minouHub added physics involves the physics db:exomol related to the ExoMol database labels Apr 22, 2026
@MJO7

MJO7 commented Apr 27, 2026

Copy link
Copy Markdown
Author

Thanks for the response @minouHub! I'm actively working on all of these reviews sequentially.

One quick question on test_exomolapi.py: should I drop the mock-based unit tests entirely and rely on the existing @pytest.mark.needs_connection test for broadening coverage, or keep them but simplify the patching?

@github-actions

Copy link
Copy Markdown

⚠️ Inactivity Notice

This pull request has been inactive for 60 days. To keep the project's workflow efficient, it may be closed by an admin if no further updates (commits, comments, or reviews) are made soon.

If you're still working on this:

  • Push new commits or address feedback.
  • Leave a comment with an update or timeline.

If this PR is no longer needed, feel free to close it yourself. Thanks for your contribution!

@github-actions github-actions Bot added inactive-notice Inactivity notice posted and removed inactive-notice Inactivity notice posted labels Jun 29, 2026
@minouHub minouHub modified the milestones: 0.17.1, 0.18 Jul 19, 2026
@github-actions github-actions Bot added the inactive-notice Inactivity notice posted label Sep 21, 2026
@github-actions

Copy link
Copy Markdown

⚠️ Inactivity Notice

This pull request has been inactive for 60 days. To keep the project's workflow efficient, it may be closed by an admin if no further updates (commits, comments, or reviews) are made soon.

If you're still working on this:

  • Push new commits or address feedback.
  • Leave a comment with an update or timeline.

If this PR is no longer needed, feel free to close it yourself. Thanks for your contribution!

@github-actions github-actions Bot removed the inactive-notice Inactivity notice posted label Sep 28, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

db:exomol related to the ExoMol database physics involves the physics

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants