Instruments & Spectrum Loading¶
unite splits instrumental configuration and spectral data into two modules:
unite.instrument— disperser classes and calibration tokens (hardware configuration)unite.spectrum— spectrum containers and loader functions (data I/O)
This separation means you can configure and serialize dispersers independently of any particular dataset, then load spectra against those configurations.
Dispersers¶
A disperser describes an instrument’s spectral characteristics:
R(λ) — the resolving power as a function of wavelength, used to compute the LSF FWHM at each pixel.
(dλ/dpix)(λ) — the wavelength dispersion per pixel as a function of wavelength, used to convert between pixel offsets and wavelength offsets.
Calibration Tokens¶
For a given instrument the user can optionally attach calibration tokens with associated priors:
RScale: Resolution scaling factor (e.g. R_eff = R_nominal x RScale)FluxScale: Flux scaling factor (e.g. F_eff = F_model x FluxScale)PixOffset: Detector pixel displacement (e.g. λ_eff = λ_model − (dλ/dpix)(λ_model) x PixOffset)
These absorb calibration offsets and uncertainties between instruments.
from unite.instrument import RScale, FluxScale, PixOffset
In practice FluxScale and PixOffset are most useful for relative calibration between
spectra — one spectrum is treated as the reference with fixed calibration, and the other spectra
have free parameters to account for differences in flux calibration and wavelength solution. If a
FluxScale is applied to all spectra it is completely degenerate with the overall flux
normalization of the model.
RScale — Resolution Calibration¶
Multiplies the nominal \(R(\lambda)\) by a free factor: \(R_\mathrm{eff}(\lambda) = R_\mathrm{nominal}(\lambda) \times \text{r\_scale}\)
r = RScale(prior=prior.TruncatedNormal(1.0, 0.05, 0.8, 1.2))
Useful when the actual LSF is broader or narrower than the calibration predicts — e.g., for extended sources in NIRSpec, or when the slit filling fraction differs from the calibration assumption.
FluxScale — Relative Flux Calibration¶
Multiplies the model flux for all pixels in a spectrum by a free factor. A value of 1.0 means no correction.
f = FluxScale(prior=prior.Uniform(0.5, 2.0))
PixOffset — Wavelength Offset¶
Encodes the displacement of the spectrum on the detector relative to the wavelength calibration,
in pixels. A positive value means the spectrum is displaced toward longer wavelengths: the model
subtracts pix_offset x (dλ/dpix) from the calibrated pixel-edge wavelengths, shifting them
blueward to compensate.
For example, if a disperser consistently returns redshifts that are too high compared to a
reference (the calibrated wavelengths are too long), set a positive PixOffset to correct it:
p = PixOffset(prior=prior.Uniform(-2, 2))
Parameter Naming¶
Each calibration token accepts an optional semantic label as its first argument. A type-specific prefix is automatically prepended to form the final site name:
RScale('shared')→ site name'r_scale_shared'(prefix:r_scale_)FluxScale('shared')→ site name'flux_scale_shared'(prefix:flux_scale_)PixOffset('shared')→ site name'pix_offset_shared'(prefix:pix_offset_)
Pass only the semantic label, not the full prefixed name. For unshared tokens on a single named
disperser, the disperser name is used automatically (e.g., RScale() on 'G235H' →
'r_scale_G235H').
Fixed Calibration¶
Use Fixed to apply a known correction without uncertainty. This is the
default for all calibration tokens — if you don’t specify a token the disperser is treated as the
reference with no correction.
g395h = nirspec.G395H(flux_scale=FluxScale(prior=prior.Fixed(1.15)))
JWST/NIRSpec¶
Convenience classes are provided for each NIRSpec disperser:
from unite.instrument import nirspec
# nirspec.G140H, nirspec.G140M, nirspec.G235H, nirspec.G235M,
# nirspec.G395H, nirspec.G395M, nirspec.PRISM
NIRSpec dispersers support two resolution calibration modes via r_source:
g235h = nirspec.G235H(r_source='point') # point-source R (de Graaff et al. 2024)
g235h = nirspec.G235H(r_source='uniform') # uniform illumination R from FITS tables
'point'(default): Uses polynomial fits to the slit-centered point-source resolving power derived from de Graaff et al. (2024). Recommended for most sources. Even resolved sources can use this curve combined with anRScaletoken to account for the effective slit filling fraction or position.'uniform': Uses tabulated resolving power for uniform illumination of the slit from JDOX.
Here is an example applying calibration tokens to NIRSpec dispersers, using realistic offsets between G395M and PRISM based on the RUBIES survey (de Graaff et al. 2025):
from unite.instrument import nirspec, RScale, FluxScale, PixOffset
from unite import prior
# Free resolution scale shared across both dispersers
r_scale = RScale(prior=prior.TruncatedNormal(low=0.7, high=1.3, loc=1.0, scale=0.3))
# G395M is the reference — no FluxScale or PixOffset
g395m = nirspec.G395M(r_scale=r_scale)
# PRISM has free flux and wavelength offsets relative to G395M
prism = nirspec.PRISM(
r_scale=r_scale,
flux_scale=FluxScale(prior=prior.TruncatedNormal(low=0.8, high=1.4, loc=1.1, scale=0.1)), # de Graaff+25 Fig 8
pix_offset=PixOffset(prior=prior.TruncatedNormal(low=-0.2, high=0.6, loc=0.2, scale=0.1)), # de Graaff+25 Fig 9
)
SDSS¶
The SDSS disperser computes \(R(\lambda)\) from the wdisp column in the native SDSS pipeline
output. The disperser’s resolution grid is populated at load time by from_sdss_fits().
from unite.instrument import sdss
disperser = sdss.SDSSDisperser()
# Calibration tokens can be attached here as with any disperser
Generic Dispersers¶
For instruments without built-in support, unite provides two generic disperser classes.
Import them explicitly from unite.instrument.generic.
SimpleDisperser¶
SimpleDisperser defines the pixel scale from the input
wavelength array (one wavelength element per pixel). The resolution can be a constant or an array.
from unite.instrument import generic
import numpy as np
from astropy import units as u
wavelength = np.linspace(4000, 9000, 500) * u.AA
disperser = generic.SimpleDisperser(
wavelength=wavelength,
R=3000.0,
name='my_spectrograph',
)
Three modes are supported for specifying the dispersion:
Mode |
Argument |
Description |
|---|---|---|
Resolving power |
|
\(R = \lambda / \Delta\lambda\) (constant or array) |
Wavelength dispersion |
|
\(\Delta\lambda\) per pixel (constant or array) |
Velocity dispersion |
|
\(\Delta v\) per pixel in km/s (constant or array) |
GenericDisperser¶
For maximum flexibility, pass callables that return \(R(\lambda)\) and \(d\lambda/\mathrm{pix}(\lambda)\):
from unite.instrument import generic
def my_R(wavelength):
return 2000 + 500 * (wavelength - 4000) / 5000
def my_dlam(wavelength):
return wavelength / my_R(wavelength)
disperser = generic.GenericDisperser(R_func=my_R, dlam_dpix_func=my_dlam, unit=u.AA, name='custom')
InstrumentConfig & Serialization¶
InstrumentConfig wraps a set of dispersers for serialization
and named lookup:
from unite.instrument.config import InstrumentConfig
dc = InstrumentConfig([prism, g395m])
Dispersers can be retrieved by name:
d = dc['G395M'] # returns the G395M disperser
d = dc['PRISM'] # returns the PRISM disperser
names = dc.names # ['PRISM', 'G395M']
This is particularly useful after loading a configuration from YAML:
dc = InstrumentConfig.load('dispersers.yaml')
g395m = dc['G395M'] # retrieve with calibration tokens intact
validate() is called automatically on construction and warns if any calibration axis has no
fixed anchor — meaning all dispersers have a free parameter on that axis, making it
unidentifiable.
Instrument configs can be combined with addition:
dc1 = InstrumentConfig.load('dispersers1.yaml')
dc2 = InstrumentConfig.load('dispersers2.yaml')
dc_combined = dc1 + dc2
Loading Spectra¶
All spectrum loaders live in unite.spectrum and return a Spectrum
object. Loaders always require a configured disperser.
from_arrays¶
from_arrays() is the generic loader — it works with any disperser and
accepts astropy.units.Quantity arrays for the pixel bin edges, flux, and flux error.
Because unite performs exact pixel integration, bin edges (not centers) are required.
from unite.spectrum import from_arrays
from astropy import units as u
import numpy as np
low = wl_edges[:-1] # lower pixel edges, Quantity
high = wl_edges[1:] # upper pixel edges, Quantity
flux = flux_array * (1e-20 * u.erg / (u.s * u.cm**2 * u.AA))
err = err_array * (1e-20 * u.erg / (u.s * u.cm**2 * u.AA))
spectrum = from_arrays(low, high, flux, err, disperser=disperser)
This is the right loader for:
Simulated spectra or pipeline-reduced data in custom formats
Any instrument not covered by
from_DJAorfrom_sdss_fitsQuick testing with synthetic data
An optional name argument overrides the disperser name on the resulting spectrum.
from_DJA¶
from_DJA() loads a NIRSpec spectrum in the format of the
DAWN JWST Archive. It accepts any configured
disperser and can load directly from a URL (with optional astropy caching).
from unite.instrument import nirspec
from unite.spectrum import from_DJA
g235h = nirspec.G235H()
spectrum = from_DJA('spectrum_g235h.fits', disperser=g235h)
spectrum = from_DJA('https://url_to_dja_spectrum.fits', disperser=g235h, cache=True)
from_sdss_fits¶
from_sdss_fits() loads an SDSS spectrum from a native pipeline FITS file
(or URL). It requires an SDSSDisperser — the disperser’s
\(R(\lambda)\) grid is populated from the wdisp column in the file.
from unite.instrument import sdss
from unite.spectrum import from_sdss_fits
disperser = sdss.SDSSDisperser()
spectrum = from_sdss_fits('spec-PLATE-MJD-FIBER.fits', disperser=disperser)
Mixing Instruments¶
unite can combine spectra from entirely different instruments in one fit. Each spectrum uses its
own disperser; shared line tokens ensure the same physical parameters are used across all spectra.
from unite.instrument import nirspec, sdss, FluxScale
from unite import prior
from unite.spectrum import Spectra, from_DJA, from_sdss_fits
# NIRSpec grating — flux reference, no FluxScale
g395h = nirspec.G395H()
# SDSS disperser with a free flux scale relative to NIRSpec
sdss_disp = sdss.SDSSDisperser(
flux_scale=FluxScale(prior=prior.Uniform(0.5, 2.0))
)
spec_nir = from_DJA('g395h.fits', disperser=g395h)
spec_sdss = from_sdss_fits('spec-PLATE-MJD-FIBER.fits', disperser=sdss_disp)
spectra = Spectra([spec_nir, spec_sdss], redshift=z_sys)