# 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

{class}`~unite.line.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.

```python
from unite import line, prior
from astropy import units as u

lc = line.LineConfiguration()
```

---

## Adding Lines

### Basic Usage

```python
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 {class}`astropy.units.Quantity` with wavelength units:

```python
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 ({class}`~unite.line.Redshift`)
- A **flux** token ({class}`~unite.line.Flux`)
- A **profile shape** (e.g. `Gaussian`, `PseudoVoigt`, etc.)
- **FWHM token(s)** appropriate for the chosen profile
- Additional shape parameters (e.g. `h3`, `h4` for `GaussHermite`)
- 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`.

```python
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 {doc}`instrument`. 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`.

```python
# 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.

```python
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.

```python
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 |
|-------|------|------|
| {class}`~unite.line.Redshift` | Redshift offset from systemic | dimensionless |
| {class}`~unite.line.FWHM` | Line width | km/s |
| {class}`~unite.line.Flux` | Line flux normalization | internal units |
| {class}`~unite.line.LineShape` | 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 {doc}`build_model` for details.
:::

### Shared Kinematics

```python
# One redshift, one FWHM → shared across all lines
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, 10)))
lc.add_line('NII_6585', 6585.0 * u.AA, redshift=z, fwhm_gauss=fwhm,
            flux=line.Flux('NII6585_flux', prior=prior.Uniform(0, 10)))
lc.add_line('NII_6549', 6549.0 * u.AA, redshift=z, fwhm_gauss=fwhm,
            flux=line.Flux('NII6549_flux', prior=prior.Uniform(0, 10)))
```

Result: one `z` parameter and one `fwhm` parameter in the model, shared by all three lines.

### Independent Parameters

```python
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:

```python
# 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 `samples` dict returned by `mcmc.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.

```python
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 {doc}`priors` for the full
reference.

```python
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 {doc}`/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 |
|--------|---------|-------------------|--------------------|-------------------|
| `'gaussian'`, `'normal'` | `Gaussian` | `fwhm_gauss` | | analytic |
| `'cauchy'`, `'lorentzian'` | `Cauchy` | `fwhm_lorentz` | | analytic |
| `'pseudovoigt'`, `'voigt'` | `PseudoVoigt` | `fwhm_gauss`, `fwhm_lorentz` | | analytic |
| `'laplace'`, `'exponential'` | `Laplace` | `fwhm_exp` | | analytic |
| `'semg'`, `'exp-gaussian'` | `SEMG` | `fwhm_gauss`, `fwhm_exp` | | analytic |
| `'hermite'`, `'gauss-hermite'` | `GaussHermite` | `fwhm_gauss` | `h3`, `h4` | analytic |
| `'split-normal'`, `'two-sided'` | `SplitNormal` | `fwhm_blue`, `fwhm_red` | | analytic |
| `'skew-voigt'`, `'skewvoigt'` | `SkewVoigt` | `fwhm_gauss`, `fwhm_lorentz` | `alpha` | midpoint rule |

### 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)

```python
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_gauss` before 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)

```python
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)

```python
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 {doc}`/derivations/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
{doc}`/derivations/gauss-hermite` for the full derivation.

**Parameters:** `fwhm_gauss` (km/s), `h3` (dimensionless), `h4` (dimensionless)

```python
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)

```python
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)
```

### SkewVoigt

A pseudo-Voigt profile multiplied by a skew factor
$[1 + \text{erf}(\alpha(x-c)/(\sqrt{2}\,\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 {doc}`/derivations/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)

```python
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)
```

---

## Merging Configurations

{class}`~unite.line.LineConfiguration` supports merging two configurations:

```python
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 transmission factor `exp(-tau * phi(lambda))` that multiplies the
emission and/or continuum flux, rather than adding flux. The key difference from
emission lines is that absorption lines use a {class}`~unite.line.Tau` (optical
depth) token instead of a {class}`~unite.line.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 {ref}`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:

```python
# 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` token controls the optical depth at line center. Higher values mean
stronger absorption:

```python
# 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`:

```python
# This raises TypeError:
lc.add_line('bad', 6563.0 * u.AA, flux=line.Flux(), tau=line.Tau())  # Error!
```

### Sharing Tau Parameters

Like `Flux`, `Redshift`, and `FWHM` tokens, `Tau` tokens can be shared across
multiple absorption lines to produce a single model parameter:

```python
tau = line.Tau('balmer_tau')
lc.add_line('HI_Ha', 6563.0 * u.AA, tau=tau)
lc.add_line('HI_Hb', 4861.0 * u.AA, tau=tau)
```

### Mixing Emission and Absorption

Emission and absorption lines coexist naturally in the same configuration:

```python
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
```

---

## Serialization

{class}`~unite.line.LineConfiguration` supports standalone YAML serialization:

```python
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 {doc}`serialization` for the full
workflow and YAML format.
