Skip to content

synthe_py — The Spectrum Synthesis Engine

synthe_py is the heart of pykurucz: a pure Python reimplementation of Kurucz's SYNTHE code that computes emergent stellar spectra wavelength by wavelength. Given a model atmosphere and a line list, it evaluates continuum opacity, accumulates line opacity from ~1.3 million atomic lines and molecular transitions, and solves the radiative transfer equation.

Purpose

The synthesis engine takes a preprocessed atmosphere (.npz) and produces a .spec file containing:

  • Wavelength (nm, vacuum)
  • Total emergent flux \(F_\lambda\) (line + continuum)
  • Continuum-only flux \(F_{\rm cont}\)

It is agnostic about the origin of the atmosphere — whether from atlas_py, MARCS, PHOENIX, or any other source — as long as the input follows the Kurucz .atm / .npz conventions.

Key Modules

engine/opacity.py

This is the main synthesis driver (run_synthesis). It implements:

  1. Wavelength grid generation — geometric spacing based on resolving power
  2. Atmosphere loading — prefers .npz caches, auto-refreshes stale versions
  3. Line list compilation — reads atomic and molecular catalogs, compiles fort.12-style records
  4. Continuum opacity evaluation — per-wavelength, per-depth
  5. Line opacity accumulation — Voigt profile wing addition for atomic and molecular lines
  6. Radiative transfer dispatch — calls solve_lte_spectrum for each wavelength batch

Fortran parity in opacity.py

The line-opacity kernels (_accumulate_metal_profile_kernel, _accumulate_mol_wings_batch) are JIT-compiled with Numba and contain inline comments mapping every loop index and branching condition to the original Fortran labels (e.g., synthe.for 320-350).

engine/radiative.py

Implements the JOSH solver for the radiative transfer equation. Given total opacity (acont + aline + sigmac + sigmal) and source functions, it returns the emergent flux at the top of the atmosphere. See Radiative Transfer for the physics details.

io/lines/

  • io/lines/atomic.py — Reads gfallvac.latest and other atomic catalogs
  • io/lines/molecular_compiler.py — Compiles molecular lines from Kurucz ASCII and binary formats (Schwenke TiO, Partridge–Schwenke H₂O)
  • io/lines/compiler.py — Builds the unified fort.12-style record list used by the opacity kernel

physics/

Module Purpose
continuum.py H⁻, H I, He, metal, and scattering continuous opacity
profiles.py Voigt profile evaluation (Faddeeva function)
hydrogen_wings.py Stark-broadened hydrogen line wings
helium_profiles.py Tabulated helium line broadening
populations.py Saha–Boltzmann and partition functions
voigt_jit.py Numba-accelerated Voigt kernels

Two-Stage Design

synthe_py is deliberately split into two stages:

  1. Preprocessing (convert_atm_to_npz.py) — Solves the equation of state (populations, molecular equilibrium, continuous opacity) once per atmosphere.
  2. Wavelength loop (synthe_py.cli) — Repeats the line-opacity and RT steps for every wavelength point.

This separation means you can synthesize many different wavelength ranges or resolutions from the same atmosphere without recomputing populations.

.atm file model atmosphere convert_atm_to_npz EOS once per atmosphere .npz cache populations synthe_py wavelength loop .spec .spec .spec 300–400 nm 500–600 nm 300–1800 nm

Parallelism Model

The most expensive operation in synthesis is the line-opacity accumulation, which is \(\mathcal{O}(N_\lambda \times N_{\rm lines})\). synthe_py parallelizes this at two levels:

Wavelength-level parallelism

The wavelength grid is partitioned into chunks, each processed by a worker in a ThreadPoolExecutor. Each worker:

  1. Receives its subset of wavelengths
  2. Evaluates continuum opacity for its subset
  3. Searches the line list for lines near its wavelengths
  4. Accumulates Voigt profile wings into a local opacity buffer
  5. Solves the RT equation (JOSH) for each wavelength

Numba JIT within workers

The inner loops (Voigt evaluation, wing accumulation, JOSH integration) are JIT-compiled by Numba. This gives single-thread performance comparable to compiled Fortran, while the process pool exploits multi-core parallelism.

Worker count tuning

By default, synthe_py uses all logical CPUs. For machines with many cores (e.g., 64+), memory bandwidth can become the bottleneck. In that case, reduce --n-workers to the number of physical cores.

Config System

synthe_py uses dataclasses defined in config.py:

from synthe_py.config import SynthesisConfig, WavelengthGrid, LineDataConfig

cfg = SynthesisConfig(
    wavelength_grid=WavelengthGrid(
        start=300.0, end=1800.0, resolution=300_000.0
    ),
    line_data=LineDataConfig(
        atomic_catalog=Path("lines/gfallvac.latest"),
        molecular_line_dirs=[Path("data/molecules")],
        include_tio=True,
        include_h2o=False,
    ),
    atmosphere=AtmosphereInput(model_path=Path("model.atm")),
    output=OutputConfig(spec_path=Path("spectrum.spec")),
    cutoff=1e-3,
    n_workers=None,  # auto = all CPUs
)

Next Steps

  • Read about atlas_py, the atmosphere engine that produces the input .atm.
  • Explore the Emulator for the warm-start stage.
  • See Data Flows for how .atm, .npz, and .spec relate.