Configuration Serialization

unite configurations can be saved to and loaded from human-readable YAML files. This supports reproducible fits, offline editing, and sharing configurations between collaborators.


The Configuration Container

Configuration is the top-level object that bundles everything needed to describe a fit:

from unite.config import Configuration
from unite.instrument.config import InstrumentConfig

config = Configuration(
    lines=lc,           # LineConfiguration
    continuum=cc,       # ContinuumConfiguration (optional)
    dispersers=dc,      # InstrumentConfig (optional)
)

Saving and Loading

Top-Level Configuration

# To a file
config.save('my_fit.yaml')

# To a YAML string (useful for logging or embedding in notebooks)
yaml_str = config.to_yaml()

# From a file
config2 = Configuration.load('my_fit.yaml')

# From a YAML string
config2 = Configuration.from_yaml(yaml_str)

# Access the reconstructed objects
lc2 = config2.lines
cc2 = config2.continuum
dc2 = config2.dispersers

Sub-Configuration Serialization

Each sub-configuration also supports standalone YAML I/O:

# Lines only
lc.save('lines.yaml')
lc2 = LineConfiguration.load('lines.yaml')

# Continuum only
cc.save('continuum.yaml')
cc2 = ContinuumConfiguration.load('continuum.yaml')

# Dispersers only
dc.save('dispersers.yaml')
dc2 = InstrumentConfig.load('dispersers.yaml')

This is useful for reusing the same line configuration across different fits, or sharing disperser setups between collaborators.


YAML Format

The YAML file is human-readable and editable. Here is the output of a simple two-line configuration:

lines:
  lines:
    - name: H_alpha
      center: 6563.0 AA
      profile: Gaussian
      redshift:
        name: nlr_z
        prior: {type: Uniform, low: -0.005, high: 0.005}
      fwhm_gauss:
        name: nlr_fwhm
        prior: {type: Uniform, low: 1.0, high: 10.0}
      flux:
        name: Ha_flux
        prior: {type: Uniform, low: 0, high: 5}
    - name: NII_6585
      center: 6585.0 AA
      profile: Gaussian
      redshift: {ref: nlr_z}       # ← shared token reference
      fwhm_gauss: {ref: nlr_fwhm}  # ← shared token reference
      flux:
        name: NII6585_flux
        prior: {type: Uniform, low: 0, high: 5}
continuum:
  regions:
    - low: 6241.85 AA
      high: 6931.05 AA
      form: {type: Linear}

Shared tokens appear once as a full definition and subsequently as a {ref: name} reference. You can edit priors, add lines, or change profile types directly in the YAML.

Dependent Priors in YAML

When a prior bound references another parameter, it is serialized with ref, scale, and offset fields:

fwhm_broad:
  name: fwhm_b
  prior:
    type: Uniform
    low: {ref: fwhm_n, scale: 1.0, offset: 150.0}
    high: 5000.0

This represents the expression fwhm_narrow * 1.0 + 150.0 as the lower bound.

Dispersers in YAML

When a InstrumentConfig is included, calibration tokens are also serialized:

dispersers:
  calib_params:
    r_shared:
      type: RScale
      prior: {type: TruncatedNormal, loc: 1.0, scale: 0.05, low: 0.8, high: 1.2}
  entries:
    - type: G235H
      r_source: point
      r_scale: {ref: r_shared}
    - type: G395H
      r_source: point
      r_scale: {ref: r_shared}
      flux_scale:
        name: flux_g395h
        prior: {type: Uniform, low: 0.5, high: 2.0}

Combining Configurations

Configurations (and sub-configurations) can be combined using the + operator. This is useful when building complex fits from modular pieces — for example, combining line configs from different spectral regions or adding dispersers incrementally.

Combining Full Configurations

# Create two independent configurations
cfg1 = Configuration(lines_1, continuum_1, dispersers_1)
cfg2 = Configuration(lines_2, continuum_2, dispersers_2)

# Combine them
cfg_combined = cfg1 + cfg2

The combined configuration contains:

  • All lines from both configs

  • All continuum regions from both configs (if present)

  • All dispersers from both configs (if present)

Combining Sub-Configurations

Each sub-config also supports the + operator for standalone combination:

# Combine line configs
lines_combined = lc1 + lc2

# Combine continuum configs
continuum_combined = cc1 + cc2

# Combine disperser configs
dispersers_combined = dc1 + dc2

Handling Missing Sub-Configs

When combining configs where one has continuum/dispersers and the other doesn’t, the non-None config is used:

cfg1 = Configuration(lines_1, continuum_1)  # has continuum
cfg2 = Configuration(lines_2)               # no continuum

cfg_combined = cfg1 + cfg2
# cfg_combined.continuum is continuum_1

Strict Mode — Name Collisions

Configuration combination uses strict mode, raising an error if any names collide:

# This raises ValueError if lc1 and lc2 have lines with the same name
try:
    cfg_combined = cfg1 + cfg2
except ValueError as e:
    print(f"Name collision: {e}")

Rename conflicting tokens before combining, or use sub-configuration merging with strict=False if you want to share tokens by name:

# LineConfiguration.merge() supports strict=False
lines_combined = lc1.merge(lc2, strict=False)  # share same-named tokens

Currently this is only supported for line configurations.

Combined Config Serialization

Combined configs serialize and deserialize like any other:

cfg_combined = cfg1 + cfg2
cfg_combined.save('combined.yaml')
cfg_restored = Configuration.load('combined.yaml')

Round-Trip Guarantee

unite tests confirm that Configuration.load(config.save(...)) reproduces an identical configuration, including:

  • All token names and priors

  • Token sharing relationships (same token → same model parameter)

  • Dependent prior expressions (ParameterRef chains)

  • Profile types and parameters

  • Continuum region boundaries and functional forms

  • Calibration token types and priors on dispersers


Editing Configurations Offline

A common workflow is to build a configuration programmatically, save it, edit the YAML by hand (e.g., to tighten priors based on a preliminary fit), and reload:

# First pass — broad priors
config.save('fit_v1.yaml')

# Edit fit_v1.yaml in your editor, then:
config_v2 = Configuration.load('fit_v1.yaml')

Warning

The YAML format is part of unite’s public API, but do not rename token name fields without also updating all {ref: ...} references to that token. Broken references will raise a KeyError on load.


Dict Interface

All configuration objects also support to_dict() / from_dict() for programmatic manipulation:

d = config.to_dict()
# Modify the dict...
config2 = Configuration.from_dict(d)

This is useful for automated parameter sweeps or template-based configuration generation.