Source code for jwspecfit.lines

"""Emission-line database and line-list helpers."""

from __future__ import annotations

import numpy as np

# Rest-frame vacuum wavelengths in Angstroms.
# Sources: NIST ASD, Morton (2003), Storey & Zeippen (2000).
REST_LINES_A: dict[str, float] = {
    # UV lines
    "Lya": 1215.670,
    "NV_1": 1238.821,
    "NV_2": 1242.804,
    "NV_doublet": 1240.81,
    "NIV_1483": 1483.321,
    "NIV_1486": 1486.496,
    "NIV_doublet": 1484.91,
    "NIII_1749": 1748.646,
    "NIII_1752": 1752.160,
    "NIII_doublet": 1750.40,
    "CIV_1": 1548.187,
    "CIV_2": 1550.772,
    "CIV_doublet": 1549.48,
    "HEII_1640": 1640.42,
    "OIII_1661": 1660.809,
    "OIII_1663": 1663.48,
    "OIII_1666": 1666.15,
    "SiIII_1": 1882.71,
    "SiIII_2": 1892.03,
    "CIII]_1907": 1906.683,
    "CIII]": 1908.734,
    "CII]_2324": 2324.21346335,
    "CII]_2326": 2325.40372661,
    # UV absorption lines
    "abs_SiII1260": 1260.422,
    "abs_CII1334":  1334.532,
    "abs_SiIV1394": 1393.755,
    "abs_SiIV1403": 1402.770,
    "abs_AlII1672": 1670.787,
    # Semi-forbidden / weak
    "OIII]_2321": 2322.41306535397,
    "OIII]_2331": 2332.015190970458,
    "OII]_2471": 2471.7168502042923,
    "FeII*_2396": 2397.0897781493236,
    # Optical doublets
    "OII_3726": 3727.0917000220848,
    "OII_3729": 3729.8754212488416,
    "OII_doublet": 3728.4800597199715,
    # Higher Balmer series + blue lines
    "H10": 3798.98234713,
    "H9": 3836.47908996,
    "NeIII_3869": 3869.85677179,
    "HEI_3889": 3889.74894995,
    "H8": 3890.16605856,
    "HEPSILON": 3971.20218326,
    "HEI_4027": 4027.327,
    # Balmer series
    "HDELTA": 4104.049910602101,
    "HEI_4145": 4144.928,
    # He II Pickering 11->4: 4199.83 Å (air, NIST) -> 4201.013 Å (vacuum)
    "HEII_4200": 4201.013,
    "HGAMMA": 4342.904611871652,
    "HBETA": 4864.041335024339,
    # Oxygen / Helium
    "OIII_4363": 4364.436278914932,
    "HEI_4472": 4472.73381586,
    "OIII_4959": 4961.6792505239255,
    "OIII_5007": 5009.636990676784,
    # Nitrogen, Helium
    "NII_5756": 5757.786514027234,
    # CIV optical doublet (Wolf-Rayet / AGN feature):
    # 2p²P° → 3s²S transition, vacuum wavelengths.
    "CIV_5803": 5802.97,
    "CIV_5814": 5813.62,
    "HEI_5877": 5878.88092174501,
    "OI_6302": 6302.046,
    "NII_6549": 6551.669402278604,
    "Ha": 6566.421366618156,
    "NII_6585": 6587.088921119292,
    "HEI_6680": 6680.000,
    # Sulphur
    "SII_6718": 6720.149693980653,
    "SII_6732": 6734.532561984591,
    "HEI_7067": 7067.138,
    "ArIII_7138": 7137.770,
    # Blue diagnostic lines (4500–4750 Å region)
    "FeII_4584": 4584.11922116,   
    "NIII_4642": 4641.93950581,    
    "FeIII_4660": 4659.35411192,  
    "HeII_4687": 4687.02143223,   
    "ArIV_4713": 4712.57819671,   
    "FeII_4732": 4732.76354118,   
    "ArIV_4741": 4741.44584045,   

}

# ---------------------------------------------------------------------------
# Pre-defined line groups for different spectral resolutions.
#
# Prism (R ~ 30–300):  merged doublet entries for unresolvable pairs.
# Grating (R ~ 1000+): individual doublet components resolved.
#
# These are the *default* line lists used when the user does not supply
# their own ``lines=`` argument.  The ``observable_lines()`` filter then
# removes anything outside the observed wavelength range.
# ---------------------------------------------------------------------------

_PRISM_LINES = [
    # UV (merged doublets at prism resolution)
    "Lya",
    "NV_doublet",
    "NIV_doublet",
    "CIV_doublet",
    "HEII_1640",
    # UV absorption lines
    "abs_SiII1260",
    "abs_CII1334",
    "abs_SiIV1394",
    "abs_SiIV1403",
    "abs_AlII1672",
    "NIII_doublet",
    "CIII]",
    # Semi-forbidden / weak UV
    "OIII]_2331",       # OIII]_2321/2331 blended at prism R; keep brighter 2331
    "OII]_2471",
    "FeII*_2396",
    # Optical (merged [OII] doublet)
    "OII_doublet",
    "H10",
    "H9",
    "NeIII_3869",
    "H8",               # HeI 3889 blended with H8 (Δ=0.4 Å); keep H8
    "HEPSILON",
    "HDELTA",
    "HGAMMA",
    "OIII_4363",
    "HEI_4472",
    "HBETA",
    "OIII_4959",
    "OIII_5007",
    "NII_5756",
    "HEI_5877",
    "NII_6549",
    "Ha",
    "NII_6585",
    "SII_6718",
    "SII_6732",
]

_GRATING_LINES = [
    # UV (individual doublet components)
    "Lya",
    "NV_1",
    "NV_2",
    "NIV_1483",
    "NIV_1486",
    "CIV_1",
    "CIV_2",
    "HEII_1640",
    # UV absorption lines
    "abs_SiII1260",
    "abs_CII1334",
    "abs_SiIV1394",
    "abs_SiIV1403",
    "abs_AlII1672",
    "OIII_1661",
    "OIII_1666",
    "NIII_1749",
    "NIII_1752",
    "SiIII_1",
    "SiIII_2",
    "CIII]_1907",
    "CIII]",
    # CII] intercombination doublet
    "CII]_2324",
    "CII]_2326",
    # Semi-forbidden / weak UV
    "OIII]_2321",
    "OIII]_2331",
    "OII]_2471",
    "FeII*_2396",
    # Optical (resolved [OII] doublet)
    "OII_3726",
    "OII_3729",
    "H10",
    "H9",
    "NeIII_3869",
    "H8",               # HeI 3889 blended with H8 (Δ=0.4 Å); unresolvable at any NIRSpec R
    "HEPSILON",
    "HEI_4027",
    "HDELTA",
    "HEI_4145",
    "HEII_4200",
    "HGAMMA",
    "OIII_4363",
    "HEI_4472",
    # Blue diagnostic lines (4500–4750 Å)
    "FeII_4584",
    "NIII_4642",
    "FeIII_4660",
    "HeII_4687",
    "ArIV_4713",        # blended with HeI 4713
    "FeII_4732",
    "ArIV_4741",
    "HBETA",
    "OIII_4959",
    "OIII_5007",
    "NII_5756",
    "CIV_5803",
    "CIV_5814",
    "HEI_5877",
    "OI_6302",
    "NII_6549",
    "Ha",
    "NII_6585",
    "HEI_6680",
    "SII_6718",
    "SII_6732",
    "HEI_7067",
    "ArIII_7138",
]


[docs] def get_line_list(grating: str = "prism") -> list[str]: """Return default line names for a given grating. For prism (R ~ 30–300), merged doublet entries are used for pairs that cannot be resolved. For medium and high-resolution gratings (R ≥ 1000), individual doublet components are returned. The user can always override by passing an explicit ``lines=`` argument to :func:`fit_lines`. Parameters ---------- grating : str One of ``"prism"``, ``"medium"`` / ``"g140m"`` / ``"g235m"`` / ``"g395m"``, ``"high"`` / ``"g140h"`` / ``"g235h"`` / ``"g395h"``, or ``"grating"`` (generic resolved mode). Returns ------- list of str Line names present in :data:`REST_LINES_A`. """ g = grating.lower() if "prism" in g: return list(_PRISM_LINES) # All gratings (medium *and* high) use resolved individual components. if any(k in g for k in ( "medium", "high", "grating", "stack", "g140m", "g235m", "g395m", "g140h", "g235h", "g395h", )): return list(_GRATING_LINES) return list(_PRISM_LINES)
[docs] def observable_lines( line_names: list[str], z: float, wave_min_um: float, wave_max_um: float, *, margin_sigma: float = 3.0, sigma_um: float = 0.005, ) -> list[str]: """Filter lines to those observable in the wavelength range. Parameters ---------- line_names : list of str Candidate line names (keys of :data:`REST_LINES_A`). z : float Source redshift. wave_min_um, wave_max_um : float Observed wavelength range in microns. margin_sigma : float Number of sigma margin from the edges. sigma_um : float Approximate line width in microns (for margin calculation). Returns ------- list of str Lines whose observed wavelength falls within the range. """ margin = margin_sigma * sigma_um lo = wave_min_um + margin hi = wave_max_um - margin # Exclude lines blueward of NV (IGM-absorbed region). # Lya needs special treatment (see lyman_alpha module) and should # never be fitted as a regular Gaussian. NV is the bluest line # that can be reliably fitted. nv_obs_um = REST_LINES_A["NV_1"] * (1 + z) * 1e-4 out = [] for name in line_names: lam_obs_um = REST_LINES_A[name] * (1 + z) * 1e-4 if lam_obs_um < nv_obs_um: continue if lo <= lam_obs_um <= hi: out.append(name) return out
[docs] def rest_wave_A(name: str) -> float: """Return rest wavelength in Angstroms for a line name. Parameters ---------- name : str Line name (key of :data:`REST_LINES_A`). Returns ------- float Rest wavelength in Angstroms. Raises ------ KeyError If the line name is not found. """ return REST_LINES_A[name]
[docs] def observed_wave_A(name: str, z: float) -> float: """Return observed wavelength in Angstroms for a line at redshift *z*.""" return REST_LINES_A[name] * (1.0 + z)
[docs] def observed_wave_um(name: str, z: float) -> float: """Return observed wavelength in microns for a line at redshift *z*.""" return REST_LINES_A[name] * (1.0 + z) * 1e-4
[docs] def show_lines( *, rest_min_A: float | None = None, rest_max_A: float | None = None, search: str | None = None, ) -> None: """Print the available emission/absorption lines in :data:`REST_LINES_A`. The printed names are the keys you can pass to the ``lines=`` or ``add_lines=`` arguments of :func:`~jwspecfit.plotting.plot_spectrum_interactive` (and to the fitter), grouped by wavelength region. Parameters ---------- rest_min_A, rest_max_A : float, optional Restrict to lines with rest wavelengths in this range (Angstroms). search : str, optional Case-insensitive substring filter on the line name. Examples -------- >>> import jwspecfit >>> jwspecfit.show_lines(rest_min_A=4000, rest_max_A=5100) >>> jwspecfit.show_lines(search="Fe") """ rows = [] for name, rest_A in REST_LINES_A.items(): if rest_min_A is not None and rest_A < rest_min_A: continue if rest_max_A is not None and rest_A > rest_max_A: continue if search is not None and search.lower() not in name.lower(): continue rows.append((name, rest_A)) if not rows: print("No lines match the given filters.") return rows.sort(key=lambda r: r[1]) regions = [ ("UV (< 3000 Å)", lambda w: w < 3000), ("Optical (3000–7500 Å)", lambda w: 3000 <= w < 7500), ("Near-IR (≥ 7500 Å)", lambda w: w >= 7500), ] name_w = max(len(n) for n, _ in rows) print(f"{'name':<{name_w}} rest λ (Å)") print("-" * (name_w + 14)) for region_name, pred in regions: sub = [r for r in rows if pred(r[1])] if not sub: continue print(f"\n# {region_name}") for name, rest_A in sub: print(f"{name:<{name_w}} {rest_A:10.3f}") print( "\nUse with: plot_spectrum_interactive(..., z=..., " 'add_lines=["NAME", ...])\n' " or: plot_spectrum_interactive(..., z=..., " "lines=[\"NAME\", ...]) # replaces defaults" )