Skip to content

pyathena tests + microphysics regression suite + O I CT dispatch fix - #67

Merged
jeonggyukim merged 6 commits into
photchem-fullfrom
pyathena-ct-mvp
Jun 8, 2026
Merged

pyathena tests + microphysics regression suite + O I CT dispatch fix#67
jeonggyukim merged 6 commits into
photchem-fullfrom
pyathena-ct-mvp

Conversation

@jeonggyukim

@jeonggyukim jeonggyukim commented Jun 8, 2026

Copy link
Copy Markdown
Owner

Description

Sub-PR into #48 photchem-full. Adds a pytest test suite for the four microphysics rate modules and fixes one bug that the new tests caught. Does not wire charge transfer into the chemistry sweep -- that follows on a separate sub-branch pyathena-ct-wiring.

Bug fix

ct_rate.py referenced a misspelled method name on the dispatch path for oxygen, so the oxygen-with-hydrogen charge-transfer ionization rate raised AttributeError. Renamed to match the actual method.

New tests under tests/

Four files cover the four rate modules in pyathena/microphysics/: charge-transfer rates (ct_rate.py), photoionization cross sections (photx.py), radiative and dielectronic recombination (rec_rate.py), and collisional ionization rates (ci_rate.py). Each file checks the indexing conventions used by the rate functions, threshold and cutoff behavior, the sign of the temperature dependence, and published reference values for a few representative ions. Test docstrings cite the relevant atomic-data sources (Verner et al. 1996, Badnell, Voronov 1997, Kingdon & Ferland 1996, Draine 2011, Stancil et al. 1999) and describe what each test checks.

Test infrastructure

tests/README.md documents how to run the suite. tests/conftest.py provides fixtures for a figures output directory, a --no-figures command-line option, and a per-ion color helper that matches PhotChem._set_colors so plots from tests use the same colors as plots from PhotChem.plt_rate_coeffs. tests/figures/ is git-ignored.

Diagnostic plots

Each rate-module test file writes a diagnostic plot under tests/figures/.

Test plan

pytest tests/ from the repo root. See tests/README.md.

Scope boundary

This PR adds tests and one bug fix only. Wiring charge transfer into evolve_one_species, the cross-element coupling with hydrogen, and the heating term in cool.py will follow on pyathena-ct-wiring.

jeonggyukim added a commit that referenced this pull request Jun 8, 2026
photchem-full was opened 2025-02-25 (PR #48) and had not been
updated since `Add PhotChem class` (58c6405). master moved 131
commits forward in the interim, including:

- numpy 2 migration (numpy>=2, yt>=4.4) + removed-API fixes
- SB99 dataset refactor + Zari+22 SFH support
- ISRF API rewrite
- find_files.py / load_sim.py docstring + behavior cleanups
- README rewrites; conda/miniforge install docs
- Various docs / autodoc improvements

Dry-run on pyathena-ct-mvp showed zero conflicts (photchem-full's
new files under data/microphysics/ + pyathena/microphysics/ touch
disjoint paths from master's changes). Confirmed by re-running this
merge with --no-commit -- same clean auto-merge here.

The sibling sub-PR #67 (pyathena-ct-mvp) will rebase on this state
before merging.
Static method `get_ct_ion_HII_OI_Draine11` was misnamed
`get_ct_ion_OI_HII_Draine11` at the dispatch site (line 183), causing
`AttributeError` when O I charge-exchange ionization rate is requested.
Symptom: `get_ct_ion_rate(8, 8, T)` crashes.

Caught by new charge-exchange rate test
`test_ct_rate_balance.py::test_ct_ion_rate_finite_nonneg[8-8-O I]`.
Pin the current behavior of `ChargeTransferRate.get_ct_rec_rate` and
`get_ct_ion_rate` before downstream code wires charge-exchange into
the abundance sweep in `evolve_one_species`. Tests document:

- API conventions (which (Z, N) labels reactant vs product per method);
- Sign of the Boltzmann factor in `get_ct_ion_rate` via the N I
  endothermic pair (ct_ion must grow with T faster than ct_rec);
- Near-resonance behavior for O+H (similar T-growth, Draine 2011
  per-J-level detailed balance at high T);
- Order-of-magnitude reference at T = 1e4 K;
- Known-quirk pinning: S I CT-ion is a constant ~1e-14 placeholder in
  the Cloudy data table (`a=1e-5, b=c=d=dE=0`) -- documented so future
  rate upgrades flag this site.
- High-q fallback behavior (q > 3 in `get_ct_ion_rate` returns 0; in
  `get_ct_rec_rate` returns Dalgarno generic estimate `1.92e-9*(q+1)`).

These tests are the regression anchor for the charge-exchange
completeness fixes described in
`tigris-notes/docs-claude/tigris-ncr/pyathena_ct_fixes_plan.md`
(MVP items 1-3); the wiring of CT into the abundance sweep follows in
later commits on this branch.

Pytest infrastructure (empty `tests/__init__.py` + this test file) is
new to pyathena -- run with `pytest pyathena/microphysics/tests/`.
Relocate `pyathena/microphysics/tests/` -> `tests/microphysics/` so
new test suites (io, fields, obs, ...) can drop into sibling subdirs
without nesting inside the package. Adds:

- `tests/README.md` documenting layout, run commands, and the
  convention for adding new tests + diagnostic plots.
- `tests/conftest.py` providing:
    * `figures_dir` fixture -> `tests/figures/` (created on demand).
    * `--no-figures` pytest CLI option to skip plot generation.
    * `save_figures` fixture (True unless `--no-figures` passed).
    * `ion_color(Z, q)` helper + `ion_colors` fixture matching the
      per-ion color convention in
      `pyathena/microphysics/photchem.py:_set_colors` (H=Greys,
      He=Purples, C=Blues, N=Oranges, O=Greens, S=Reds; intensity
      by ionization stage). Tests that produce diagnostic plots
      use this so the same ion gets the same color across every
      figure in the suite and in notebooks that use
      `PhotChem.plt_rate_coeffs` / `plt_sed_sigma_pi`.
- `tests/figures/.gitignore` excluding all PNG/PDF output.

Also enriches the existing CT-rate balance test docstrings with a
provenance note clarifying that Draine 2011 and the Cloudy O+H
polynomial both fit Stancil et al. 1999 (+ Barragan et al. 2006),
so their close agreement is expected rather than independent
cross-validation. Cloudy poly is only valid for T > 200 K (see
`cloudy/source/atmdat_char_tran.cpp:103-134` reference comment).
Three new test files under `tests/microphysics/`, mirroring the
existing `test_ct_rate_balance.py` style: smoke tests for finiteness
and sign, reference-value spot-checks against published numbers,
shape / monotonicity probes, and a diagnostic-plot test per file.

`test_photx_sigma.py` (Verner+96 photoionization cross sections):

- Threshold zero-cutoff, finite + positive above threshold for an
  ion catalog covering H, He, C, N, O, S.
- Reference values: H I sigma at 13.6 eV ~ 6.3e-18, He I at 24.6
  eV ~ 7.4e-18, He II at 54.4 eV ~ 1.6e-18 (Draine 2011 Tables
  13.1-13.3; rtol 10-20%).
- High-E decay shape (~E^-3 to E^-3.5 hydrogenic asymptote).
- Round-trip `get_Eth` consistency with the sigma=0 boundary.
- Diagnostic plot: sigma_pi(E) for the full catalog, per-ion
  colored by the `photchem.py` convention.

`test_rec_rate.py` (Badnell radiative + dielectronic recomb):

- Pins the (Z, N) = initial-ion convention used by `get_rr_rate`,
  `get_dr_rate`, `get_rec_rate`. The initial ion captures the
  electron: H II rec to H I is queried as (1, 0); He II rec is
  (2, 1); O III rec is (8, 6).
- H II Case B at T = 1e4 K = 2.59e-13 cm^3/s (Draine 2011 Eq.
  14.6). Case A > Case B verified.
- DR temperature shape: O III DR rises from T=1e3 to T=1e5 K
  (Badnell 2006a Fig. 1).
- Consistency: total = RR + DR exactly for non-H ions.
- Diagnostic plot: RR (left panel) and DR (right panel) vs T for
  HII / HeII / HeIII / CII / NII / OII / OIII.

`test_ci_rate.py` (Voronov 1997 collisional ionization):

- (Z, N) = REACTANT-ion convention pinned.
- Endothermic Boltzmann sign verified (rate grows with T for H I,
  He I, O I).
- Cold cutoff: rate is exactly 0 at T = 100 K (the U > 80 branch).
- H I CI at T = 2e5 K matches Voronov fit to 10%.
- Isoelectronic ordering: O I -> O II -> O III ionization rate
  decreases with charge at fixed T = 1e5 K.
- Diagnostic plot: beta_CI(T) for the followed-ion set.

All plot tests use the shared `ion_colors` fixture from
`tests/conftest.py` and the LaTeX `\,` thin-space convention for
labels. Plot generation is on by default; pass `pytest --no-figures`
to skip.

121 tests + 5 skipped (DR not defined for Z=1 or N=0; 3 plot tests
skipped under `--no-figures`).
Replace the legend-on-the-side layout with inline rotated labels
via `pyathena.plt_tools.line_annotation.line_annotate`, matched to
each line's slope and underlined with a white-stroke patheffect so
they stay legible over neighbours. Same-color-per-ion is preserved
(via the `ion_colors` fixture), so visual identification across
figures stays consistent.

Additional polish:

* `photx_sigma_overview.png`: clamp xlim to [3, 1e3] eV and ylim to
  [1e-20, 1e-16] cm^2 so the most relevant range (HII + soft X-ray)
  fills the panel. Add minor+major log grids.
* `rec_rate_overview.png`: per-ion annotation x-coordinate chosen
  per-line to avoid overlapping labels in the dense
  Badnell-RR-decline / DR-peak regions. Grids added.
* `ci_rate_overview.png`: same pattern -- per-line annotation x in
  the steep-rise portion. Grids added.
* `ct_OH_rec_comparison.png`: keep the side legend (only 3 source
  lines + Draine per-J decomposition, dense overlap at low T makes
  inline labels overlap badly). Add grid.

No assertion changes; 124 tests + 2 skipped continue to pass.
Round of figure refinements based on visual review:

* `rec_rate_overview.png`:
    - Add S II, S III recombination -- completes the H/He/C/N/O/S
      coverage (S is in the 10-followed-coolant set and a key
      observational tracer; missing-from-figure was an omission).
    - Split y-labels: left panel `\alpha_{rr}`, right panel
      `\alpha_{dr}`.
    - Tighten ylim top to 2e-10 (was 1e-9; the upper decade was
      empty space).

* `ci_rate_overview.png`:
    - Extend the ion catalog to high charge states (C IV/VI,
      N IV/VII, O V/VIII, S VII/XII) since the T-range goes up to
      1e8 K and at those temperatures the CIE balance is dominated
      by highly-ionized species.
    - `ion_color` now accepts an optional `num_ions` arg in
      `conftest.py` so the per-ion color gradient spans
      neutral->fully-stripped (Z+1) per element when high-q stages
      appear. Low-q stages keep the photchem.py default.
    - ylim top -> 1e-6 (was 1e-7) to show the full plateau region
      at high T.

* `photx_sigma_overview.png`:
    - Add S III (Eth=34.83 eV) since S III is in the followed-ion
      set and a major observational tracer.
    - Hand-tune each ion's inline-label x-position to avoid label
      overlaps near the H I / O I (Eth ~ 13.6) and He I / C II
      (Eth ~ 24) clusters.

* `ct_OH_rec_comparison.png`:
    - ylim bottom raised 1e-12 -> 3e-11 to focus on the HII-region
      regime where all three sources are within factor of 2.

* Whitespace normalization: collapse all "  " (multiple spaces) after
  commas and colons to single space across all four test files,
  matching the surrounding pyathena code style.

127 tests + 2 skipped pass.
@jeonggyukim
jeonggyukim marked this pull request as ready for review June 8, 2026 09:50
@jeonggyukim
jeonggyukim merged commit e336932 into photchem-full Jun 8, 2026
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.

1 participant