Line Configuration¶
unite’s line configuration system is designed to be flexible and feature rich, allowing user to set up complex multi-line models with shared and correlated parameters across a wide range of line profiles. This page walks though the core concepts and usage patterns for building line configurations.
Creating a Line Configuration¶
LineConfiguration is the core container that defines which emission
lines to fit, their profile shapes, and how parameters are shared between lines.
The best way to get started is to create an empty configurations. We’ll also need
the prior module for defining priors on line parameters, and astropy.units for specifying line wavelengths.
from unite import line, prior
from astropy import units as u
lc = line.LineConfiguration()
Adding Lines¶
Basic Usage¶
lc.add_line('H_alpha', 6563.0 * u.AA)
Important
Line names must be unique within a LineConfiguration. Adding a second line with the same name raises a ValueError. Use distinct names for different kinematic components, e.g. 'Ha_narrow' and 'Ha_broad'.
At minimum each line requires:
A name (string) — used in results tables and YAML output
A rest-frame center wavelength
Centers must be astropy.units.Quantity with wavelength units:
lc.add_line('Ly_alpha', 1216.0 * u.AA, ...)
lc.add_line('CO_10', 2.6 * u.um, ...)
Adding a Single Line¶
Optionally each line can also include:
A relative redshift token (
Redshift)A flux token (
Flux)A profile shape (e.g.
Gaussian,PseudoVoigt, etc.)FWHM token(s) appropriate for the chosen profile
Additional shape parameters (e.g.
h3,h4forGaussHermite)A strength parameter which multiplies the flux token, mostly used for line ratios (see below)
If not specified, default priors are used for redshift, flux, and FWHM tokens, and the profile defaults to Gaussian.
z = line.Redshift('z', prior=prior.Uniform(-0.01, 0.01))
fwhm = line.FWHM('fwhm', prior=prior.Uniform(50, 500))
lc.add_line(
'H_alpha', 6563.0 * u.AA,
redshift=z,
fwhm_gauss=fwhm,
flux=line.Flux('Ha_flux', prior=prior.Uniform(0, 1)),
)
You can preview your line configuration by printing it:
print(lc)
LineConfiguration: 1 lines, 1 flux / 1 z / 1 profile params
Name Wavelength Profile Redshift Params Flux Strength
------- ---------------- -------- -------- ------ ------- --------
H_alpha 6563.00 Angstrom Gaussian z fwhm Ha_flux 1.00
Redshift:
z Uniform(low=-0.01, high=0.01)
Params (fwhm_gauss):
fwhm Uniform(low=50.0, high=500.0)
Flux:
Ha_flux Uniform(low=0.0, high=1.0)
Note
Redshift tokens/priors are relative to the systemic system redshift which is specified with the Instruments & Spectrum Loading. This allows configurations to be redshift-agnostic and reusable across different datasets.
Adding Multiple Lines¶
unite also supports adding multiple lines at once with add_lines. Each entry in
centers becomes an independent line with a unique auto-generated name of the form
'{name}_{center_value}' (e.g. 'OIII_4960', 'OIII_5008'). You can also supply
an explicit names array of the same length as centers.
# Auto-generated names: 'OIII_4960' and 'OIII_5008'
lc.add_lines('OIII', [4960, 5008] * u.AA, redshift=z, fwhm_gauss=fwhm)
# Explicit names
lc.add_lines('OIII', [4960, 5008] * u.AA,
names=['OIII_blue', 'OIII_red'], redshift=z)
Note how we fix the line ratio here.
flux = line.Flux('OIII')
lc.add_lines(
'OIII', [4960, 5008] * u.AA,
redshift = z, # Same redshift
fwhm = [fwhm, fwhm], # Different FWHMs (just as an example)
flux = [line.Flux(prior = prior.Fixed(flux / 3)), flux], # Fixed ratio
)
However, it is likely easier to specify multiples through the strength parameter, which multiplies the flux token when building the model. Both approaches yield the same result.
flux = line.Flux('OIII')
lc.add_lines(
'OIII', [4960, 5008] * u.AA,
flux = line.Flux(), # Default behaviour, this would mean all lines have the same flux
strength = [1/3, 1] # But we pass a strength per line which is multiplied by the flux token when building the model
)
Parameter Sharing¶
This is unite’s central design pattern. A token is a named Python object representing a
model parameter. Same Python object = same parameter in the model.
These are the following token types:
Token |
Role |
Unit |
|---|---|---|
|
Redshift offset from systemic |
dimensionless |
|
Line width |
km/s |
|
Line flux normalization |
internal units |
|
Arbitrary profile parameter (h3, h4, etc.) |
per-parameter basis |
Note
Fluxes are relative to a scale computed based on the spectrum. See Flux and Error Scales in Building the Model for details.
Independent Parameters¶
z_narrow = line.Redshift('narrow', prior=prior.Uniform(-0.01, 0.01))
z_broad = line.Redshift('broad', prior=prior.Uniform(-0.01, 0.01))
lc.add_line('Ha_narrow', 6563.0 * u.AA, redshift=z_narrow, ...)
lc.add_line('Ha_broad', 6563.0 * u.AA, redshift=z_broad, ...)
Parameter Names¶
All token classes accept an optional first positional argument that becomes a semantic label for the parameter. It is highly recommended to name your tokens — without a name, unite auto-generates one (e.g. fwhm_0, redshift_2) that becomes difficult to interpret in output.
A type-specific prefix is automatically prepended to your label to form the final site name:
Redshift('nlr')→ site name'z_nlr'(prefix:z_)FWHM('broad')→ site name'fwhm_broad'(prefix:fwhm_)Flux('Ha')→ site name'flux_Ha'(prefix:flux_)
So pass only the semantic label, not the full prefixed name:
# Auto-generated names → based on line name, less explicit
z = line.Redshift()
# Explicit semantic labels → clear, shareable across lines
z_nlr = line.Redshift('nlr') # → site name 'z_nlr'
z_blr = line.Redshift('blr') # → site name 'z_blr'
fwhm = line.FWHM('broad') # → site name 'fwhm_broad'
flux = line.Flux('Ha') # → site name 'flux_Ha'
The site names show up in:
The
samplesdict returned bymcmc.get_samples()Column names in the parameter table from
make_parameter_table()
Tip
Use short, semantic labels (without prefixes) that identify the physical component,
e.g. 'nlr', 'blr', 'broad', 'Ha', 'NII'. The type-specific prefix is added
automatically. This makes site names concise and easy to navigate in results.
Lines can share the same name but must have at least one different token, otherwise an error will be raised. This allows you to easily set up multi-component models with shared parameters.
z = line.Redshift('nlr', prior=prior.Uniform(-0.01, 0.01))
fwhm_n = line.FWHM('narrow', prior=prior.Uniform(50, 400))
fwhm_b = line.FWHM('broad', prior=prior.Uniform(500, 3000))
lc.add_line('H_alpha', 6563.0 * u.AA, redshift=z, fwhm_gauss=fwhm_n,
flux=line.Flux('Ha_n', prior=prior.Uniform(0, 10)))
lc.add_line('H_alpha', 6563.0 * u.AA, redshift=z, fwhm_gauss=fwhm_b,
flux=line.Flux('Ha_b', prior=prior.Uniform(0, 10)))
Dependent Priors¶
Prior bounds on FWHM or Flux tokens can reference other tokens, creating dependency chains that are automatically resolved at model-build time. See Priors for the full reference.
fwhm_narrow = line.FWHM('n', prior=prior.Uniform(50, 500))
fwhm_broad = line.FWHM('b', prior=prior.Uniform(fwhm_narrow + 150, 5000))
This ensures the broad component is always at least 150 km/s wider than the narrow component.
Line Profiles¶
Here we list all currently supported profiles in unite.
Most profiles are analytically integrated over pixels (exact CDF differences) and convolved
with the instrumental LSF. The exception is SkewVoigt, which uses a midpoint-rule
approximation (see below). See Core Concepts for the LSF convolution convention.
Profiles are set via the profile argument (case-insensitive strings or class instances):
String |
Profile |
FWHM Parameter(s) |
Shape Parameter(s) |
Pixel integration |
|---|---|---|---|---|
|
|
|
analytic |
|
|
|
|
analytic |
|
|
|
|
analytic |
|
|
|
|
analytic |
|
|
|
|
analytic |
|
|
|
|
|
analytic |
|
|
|
analytic |
|
|
|
|
|
analytic |
|
|
|
|
midpoint rule |
|
|
|
analytic |
|
|
|
|
analytic |
Gaussian (default)¶
The simplest and most common profile. A Gaussian intrinsic shape convolved with the Gaussian LSF; the result is also Gaussian with \(\mathrm{FWHM} = \sqrt{\mathrm{fwhm\_gauss}^2 + \mathrm{lsf\_fwhm}^2}\).
Parameters: fwhm_gauss (km/s)
lc.add_line('H_alpha', 6563.0 * u.AA, profile='Gaussian',
redshift=z, fwhm_gauss=fwhm, flux=flux)
PseudoVoigt¶
The Voigt profile — the convolution of a Gaussian and a Lorentzian — computed via two different approximations depending on integration mode:
Analytic mode (CDF-based pixel integration): uses the extended pseudo-Voigt approximation of Ida, Ando & Toraya (2000), which decomposes the profile into four components (Gaussian, Lorentzian, intermediate, and Pearson-VII) with sixth-order polynomial mixing coefficients. Achieves < 0.12% peak-height deviation from the true Voigt profile. The Gaussian LSF is added in quadrature to
fwhm_gaussbefore computing the approximation parameters.Quadrature mode (PDF evaluation at nodes): uses the exact Voigt profile computed via the Faddeeva function \(w(z) = e^{-z^2}\operatorname{erfc}(-iz)\), approximated with the Humlicek (1982) W4 rational scheme (~\(10^{-4}\) relative error across the upper half-plane).
Parameters: fwhm_gauss (km/s), fwhm_lorentz (km/s)
lc.add_line('H_alpha', 6563.0 * u.AA, profile='PseudoVoigt',
redshift=z,
fwhm_gauss=line.FWHM('g', prior=prior.Uniform(50, 500)),
fwhm_lorentz=line.FWHM('l', prior=prior.Uniform(0, 500)),
flux=flux)
Cauchy (Lorentzian)¶
A pure Lorentzian profile. Internally a PseudoVoigt with fwhm_gauss = 0.
Parameters: fwhm_lorentz (km/s)
lc.add_line('H_alpha', 6563.0 * u.AA, profile='Cauchy',
redshift=z,
fwhm_lorentz=line.FWHM('fwhm', prior=prior.Uniform(0, 1000)),
flux=flux)
Laplace (Exponential)¶
A double-exponential (Laplace) profile convolved with the Gaussian LSF.
Parameters: fwhm_exp (km/s)
SEMG — Symmetric Exponentially Modified Gaussian¶
A Gaussian convolved with a Laplace distribution. The Gaussian component (including LSF) contributes exponential wings that are symmetric about the centre. See SEMG for the closed-form CDF derivation.
Parameters: fwhm_gauss (km/s), fwhm_exp (km/s)
GaussHermite¶
A Gaussian modified by Hermite polynomial corrections using the
probabilists’ convention. h3 controls skewness; h4 controls
kurtosis. Convolution with the Gaussian LSF rescales the shape
parameters as \(h_m' = h_m\,(\sigma_g/\sigma_\text{tot})^m\). See
Gauss-Hermite for the full derivation.
Parameters: fwhm_gauss (km/s), h3 (dimensionless), h4 (dimensionless)
lc.add_line('H_alpha', 6563.0 * u.AA, profile='GaussHermite',
redshift=z,
fwhm_gauss=line.FWHM('fwhm', prior=prior.Uniform(50, 1000)),
h3=line.Param('h3', prior=prior.TruncatedNormal(0, 0.1, -0.3, 0.3)),
h4=line.Param('h4', prior=prior.TruncatedNormal(0, 0.1, -0.3, 0.3)),
flux=flux)
SplitNormal¶
A two-sided Gaussian with independent widths on the blue and red sides.
Parameters: fwhm_blue (km/s), fwhm_red (km/s)
lc.add_line('H_alpha', 6563.0 * u.AA, profile='SplitNormal',
redshift=z,
fwhm_blue=line.FWHM('b', prior=prior.Uniform(50, 500)),
fwhm_red=line.FWHM('r', prior=prior.Uniform(50, 500)),
flux=flux)
SkewNormal¶
A Gaussian (with LSF) multiplied by an erf skew factor
\([1 + \text{erf}(\alpha_\text{eff}(x-c)/w_0')]\). For alpha = 0 the profile
reduces exactly to a Gaussian.
Unlike SkewVoigt, the convolution with the Gaussian LSF is exact — the
shape parameter rescales analytically as
\(\alpha_\text{eff} = \alpha\sigma_g / \sqrt{\sigma_g^2 + (1+\alpha^2)\sigma_\text{lsf}^2}\),
with no numerical correction. Pixel integration uses the closed-form
skew-normal CDF \(\Phi(z) - 2T(z, \alpha_\text{eff})\) via Owen’s T function.
See Skew Normal for the full derivation.
Parameters: fwhm_gauss (km/s), alpha (dimensionless)
lc.add_line('H_alpha', 6563.0 * u.AA, profile='SkewNormal',
redshift=z,
fwhm_gauss=line.FWHM('fwhm', prior=prior.Uniform(50, 500)),
alpha=line.LineShape('alpha', prior=prior.TruncatedNormal(0, 2, -10, 10)),
flux=flux)
SkewVoigt¶
A pseudo-Voigt profile multiplied by a skew factor
\([1 + \text{erf}(\alpha(x-c)/w_0)]\) where \(w_0 = \Gamma_V/(2\sqrt{\ln 2}) = \sigma_V\sqrt{2}\)
is the erf scale derived from the Thompson et al. Voigt FWHM \(\Gamma_V\). For a
pure Gaussian (\(\Gamma_l = 0\)) this reduces to the standard skew-normal with shape
parameter \(\alpha\) and dispersion \(\sigma_g\). The profile integrates to 1 for
any alpha because the skew factor is odd and the pseudo-Voigt is even. Convolution
with the Gaussian LSF rescales the skewness to an effective \(\alpha_\text{eff}\)
(see Skew Voigt).
Warning
SkewVoigt is not analytically integrated over pixels. The pixel integral of the
skew correction requires Owen’s T function (Gaussian part) and a separate quadrature
(Lorentzian part) — neither reduces to standard functions. Instead, unite evaluates
the profile at each pixel midpoint and multiplies by the pixel width. This is accurate
when the profile is well-resolved (intrinsic FWHM several times the pixel width), but
introduces sub-pixel quadrature error for marginally resolved lines. Consider using
integration_mode='quadrature' as an alternative integration mode.
Parameters: fwhm_gauss (km/s), fwhm_lorentz (km/s), alpha (dimensionless)
lc.add_line('H_alpha', 6563.0 * u.AA, profile='SkewVoigt',
redshift=z,
fwhm_gauss=line.FWHM('g', prior=prior.Uniform(50, 500)),
fwhm_lorentz=line.FWHM('l', prior=prior.Uniform(0, 500)),
alpha=line.LineShape('alpha', prior=prior.TruncatedNormal(0, 2, -10, 10)),
flux=flux)
BoxGauss¶
A uniform rectangular (boxcar) distribution convolved with a Gaussian. The intrinsic
profile is constant across a velocity window of width fwhm_box and zero outside it
(area = 1). Convolution with the combined Gaussian (LSF ⊕ fwhm_gauss in quadrature)
smooths the sharp edges. The exact pixel integral is computed analytically using the
antiderivative of the Gaussian CDF.
The profile reduces to a pure Gaussian as fwhm_box → 0, and to a sharp rectangular
window as fwhm_gauss → 0 (with lsf_fwhm → 0).
Parameters: fwhm_box (km/s), fwhm_gauss (km/s)
lc.add_line('H_alpha', 6563.0 * u.AA, profile='BoxGauss',
redshift=z,
fwhm_box=line.FWHM('box', prior=prior.Uniform(100, 2000)),
fwhm_gauss=line.FWHM('g', prior=prior.Uniform(0, 500)),
flux=flux)
GaussianSplitLaplace (Asymmetric EMG)¶
A Gaussian (with LSF) convolved with a split-Laplace (asymmetric double-exponential) distribution, where the blue (short-wavelength) and red (long-wavelength) exponential tails are controlled independently. The exact pixel integral is computed analytically via the closed-form antiderivative of the Gaussian–split-Laplace CDF.
Parameters: fwhm_gauss (km/s), fwhm_l_blue (km/s), fwhm_l_red (km/s)
lc.add_line('H_alpha', 6563.0 * u.AA, profile='GaussianSplitLaplace',
redshift=z,
fwhm_gauss=line.FWHM('g', prior=prior.Uniform(50, 500)),
fwhm_l_blue=line.FWHM('lb', prior=prior.Uniform(0, 500)),
fwhm_l_red=line.FWHM('lr', prior=prior.Uniform(0, 500)),
flux=flux)
Merging Configurations¶
LineConfiguration supports merging two configurations:
lc_narrow = line.LineConfiguration()
# ... add narrow lines ...
lc_broad = line.LineConfiguration()
# ... add broad lines ...
# strict=True (default, also __add__): raises on token name collisions
lc_combined = lc_narrow + lc_broad
# strict=False: shares same-named tokens of same type
lc_combined = lc_narrow.merge(lc_broad, strict=False)
The strict=False mode is useful when both configurations define tokens with the same name
and you want them to be treated as the same model parameter. However, proceed with caution, it will not check that the priors for the same-named tokens are identical, it will choose the token from the first configuration.
Absorption Lines¶
unite supports absorption lines alongside emission lines. Absorption profiles
produce a wavelength-dependent transmission T(λ) = exp(-τ₀ · φ_norm(λ)) that
multiplies the emission and/or continuum flux, where τ₀ is the Tau
parameter and φ_norm(λ) = φ(λ) / φ(λ_center) is the profile normalized to 1
at the nominal line center. The key difference from emission lines is that absorption
lines use a Tau (optical depth) token instead of a
Flux token.
Warning
Two distinct approximations apply to absorption lines — one mode-dependent, one universal.
Analytic mode only: each profile is integrated over pixels independently before the
nonlinear transmission is applied, computing exp(-τ·∫φ) rather than ∫F·exp(-τ·φ).
This is accurate when the absorber is well-resolved but introduces an approximation for
unresolved or marginally resolved lines. Use integration_mode='quadrature' to avoid this.
Both modes: the absorption profile φ(λ) passed to exp(-τ·φ) is the LSF-convolved
profile, not the intrinsic one. The correct observable requires convolving the nonlinear
product F·exp(-τ·φ_intrinsic) with the LSF over its full multi-pixel support, which is not
currently supported. This approximation is accurate when the absorber is resolved
(intrinsic FWHM ≫ LSF FWHM) or optically thin (τ ≪ 1). For unresolved, optically thick
absorbers the inferred τ will be biased high and the curve of growth misrepresented.
See LSF Pre-Convolution of Absorption Profiles for a full discussion.
Adding Tau-Parametrized Lines¶
Tau-parametrized lines use the same profile shapes as flux-parametrized lines.
The distinction is made by passing a Tau token instead of Flux. Any profile
can be used — the profile controls the shape, tau vs flux controls how
it enters the model:
# Gaussian absorption (single FWHM parameter)
lc.add_line('HI_abs', 6563.0 * u.AA, tau=line.Tau())
# Voigt absorption (Gaussian + Lorentzian FWHM)
lc.add_line('HI_voigt', 4861.0 * u.AA, profile='voigt', tau=line.Tau())
# Lorentzian absorption (single Lorentzian FWHM)
lc.add_line('HI_lor', 4341.0 * u.AA, profile='cauchy', tau=line.Tau())
Tau vs Flux¶
The Tau parameter is the optical depth at the nominal line center of the
intrinsic (pre-LSF) profile — i.e. τ₀ = τ(λ_center) where λ_center is the
rest-frame center wavelength shifted by the line’s redshift. Concretely:
τ₀ = 1→ transmission ≈ 37% at line center (optically thin to moderate)τ₀ = 3→ transmission ≈ 5% (significantly optically thick)τ₀ ≫ 1→ effectively black at line center
For symmetric profiles (Gaussian, PseudoVoigt, Cauchy, Laplace, SEMG,
SplitNormal), τ₀ is also the maximum optical depth anywhere in the profile.
For asymmetric profiles (GaussHermite with h3 ≠ 0, SkewVoigt with large
alpha), the profile peak shifts away from the nominal center wavelength.
τ₀ remains the optical depth at the nominal center, not the absolute
maximum the profile reaches. If the physical peak depth matters for your
science case, prefer a symmetric profile for absorption lines, or account for
this distinction when interpreting posteriors.
# Explicit tau token with custom prior
tau = line.Tau('deep', prior=prior.Uniform(0, 50))
lc.add_line('HI_abs', 6563.0 * u.AA, tau=tau)
If neither flux nor tau is specified, the line defaults to emission
(with an auto-created Flux token). Passing both flux and tau raises
TypeError:
# This raises TypeError:
lc.add_line('bad', 6563.0 * u.AA, flux=line.Flux(), tau=line.Tau()) # Error!
Mixing Emission and Absorption¶
Emission and absorption lines coexist naturally in the same configuration:
lc = line.LineConfiguration()
lc.add_line('Ha', 6563.0 * u.AA) # emission (Gaussian by default)
lc.add_line('HI_abs', 6563.0 * u.AA, tau=line.Tau()) # absorption
Depth Ordering (zorder)¶
Each line carries an integer zorder that determines which tau absorbers attenuate
it. A tau absorber at depth Z only absorbs components with zorder < Z — i.e.
sources behind it.
Defaults (no arguments required for the common foreground-screen geometry):
Line type |
Default |
|---|---|
Emission ( |
|
Absorption ( |
|
With these defaults every tau absorber sits in front of all emission lines.
Pass zorder explicitly to add_line() to break from the default geometry:
# Absorber at zorder=1 (default) — absorbs Ha (zorder=0) but not AGN (zorder=2)
lc = line.LineConfiguration()
lc.add_line('Ha', 6563.0 * u.AA) # emission, zorder=0
lc.add_line('AGN_Ha', 6563.0 * u.AA, zorder=2) # emission, zorder=2 - not absorbed
lc.add_line('NaD', 5893.0 * u.AA, tau=line.Tau()) # absorber, zorder=1 (default)
The zorder column is visible when you print the configuration:
Name Wavelength Profile Redshift Params Flux/Tau zorder Strength
------- ---------- -------- -------- ------ -------- ------ --------
Ha 6563.00 Gaussian z fwhm ha 0 1.00
AGN_Ha 6563.00 Gaussian z fwhm ha_agn 2 1.00
NaD 5893.00 Gaussian z fwhm tau_nad 1 1.00
See Component Depth Ordering (zorder) in the model-building guide for the
full mapping from zorder values to physical geometry, including how to reproduce
the three classic absorber-position scenarios.
Serialization¶
LineConfiguration supports standalone YAML serialization:
lc.save('lines.yaml')
lc2 = line.LineConfiguration.load('lines.yaml')
Token sharing is preserved: if two lines shared a Redshift token before saving, they will
share the same reconstructed token after loading. See Configuration Serialization for the full
workflow and YAML format.