Repository navigation
Conversation
…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>
There was a problem hiding this comment.
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 respectradis.config["MISSING_BROAD_COEF"]. - Propagate
diluentinto the ExoMol fetch pipeline and remove the ExoMol “air-only” guard incalc. - Add/extend unit + integration tests and document the
diluentkwarg for ExoMol infetch_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.
| @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: |
There was a problem hiding this comment.
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.
| @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" | ||
|
|
There was a problem hiding this comment.
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.
| 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 |
There was a problem hiding this comment.
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.
| 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", | |
| ) |
| _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 |
There was a problem hiding this comment.
_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.
| _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) |
| 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, | ||
| ) |
There was a problem hiding this comment.
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.
| "No `.broad` file will be loaded for this species; " | ||
| "broadening will fall back to air coefficients.", |
There was a problem hiding this comment.
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.
| "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.", |
| # 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 | ||
|
|
There was a problem hiding this comment.
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).
|
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 Report❌ Patch coverage is 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:
|
minouHub
left a comment
There was a problem hiding this comment.
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"): |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Please rebase or merge develop (this issue was solved)
| from radis import SpectrumFactory | ||
|
|
||
| radis.config["MISSING_BROAD_COEF"] = False | ||
| try: |
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
No try except here. Simply execute your code
| if species not in mdb.broad_partners: | ||
| import warnings | ||
|
|
||
| warnings.warn( |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
You can move this line before the assert to make sure it is executed
|
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? |
|
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:
If this PR is no longer needed, feel free to close it yourself. Thanks for your contribution! |
|
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:
If this PR is no longer needed, feel free to close it yourself. Thanks for your contribution! |
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.