Source code for unite.config
"""Top-level configuration container for unite models.
:class:`Configuration` bundles a :class:`~unite.line.LineConfiguration`,
an optional :class:`~unite.continuum.ContinuumConfiguration`, and an optional
:class:`~unite.instrument.config.InstrumentConfig` into a single
serializable object.
All sub-configs can still be serialized individually via their own
``to_dict`` / ``from_dict`` methods. :class:`Configuration` adds
convenience for round-tripping the combined object to YAML.
Examples
--------
Build and save:
>>> from unite.line import LineConfiguration, Redshift, FWHM
>>> from unite.continuum import ContinuumConfiguration
>>> from unite.instrument import InstrumentConfig, RScale
>>> from unite.instrument.nirspec import G235H, G395H
>>> from unite.prior import TruncatedNormal
>>> import astropy.units as u
>>> z, w = Redshift('nlr'), FWHM('nlr')
>>> lines = LineConfiguration()
>>> lines.add_line('Ha', 6564.61 * u.AA, redshift=z, fwhm=w)
>>> cont = ContinuumConfiguration.from_lines(lines.wavelengths)
>>> r = RScale(prior=TruncatedNormal(1.0, 0.05, 0.8, 1.2))
>>> dispersers = InstrumentConfig([G235H(r_scale=r), G395H(r_scale=r)])
>>> cfg = Configuration(lines, cont, dispersers=dispersers)
>>> cfg.save('config.yaml')
Load and inspect:
>>> cfg2 = Configuration.load('config.yaml')
>>> cfg2.lines
LineConfiguration: ...
>>> cfg2.dispersers
InstrumentConfig: 2 disperser(s) ...
"""
from __future__ import annotations
from pathlib import Path
import yaml
from unite.continuum.config import ContinuumConfiguration
from unite.line.config import LineConfiguration
[docs]
class Configuration:
"""Container for a complete unite model configuration.
Parameters
----------
lines : LineConfiguration
Emission line configuration.
continuum : ContinuumConfiguration, optional
Continuum configuration. ``None`` if not used.
dispersers : InstrumentConfig, optional
Instrument configuration describing disperser models and calibration
tokens. When provided,
:meth:`~unite.instrument.config.InstrumentConfig.validate`
is called immediately and :class:`UserWarning` is issued if any
calibration axis lacks a fixed anchor.
Attributes
----------
lines : LineConfiguration
continuum : ContinuumConfiguration or None
dispersers : InstrumentConfig or None
"""
def __init__(
self,
lines: LineConfiguration,
continuum: ContinuumConfiguration | None = None,
dispersers=None,
) -> None:
self.lines = lines
self.continuum = continuum
self.dispersers = dispersers
if dispersers is not None:
dispersers.validate()
# ------------------------------------------------------------------
# Dict serialization
# ------------------------------------------------------------------
[docs]
def to_dict(self) -> dict:
"""Serialize to a YAML-safe dictionary.
Returns
-------
dict
Contains ``'lines'`` and, if present, ``'continuum'`` and
``'dispersers'`` keys.
"""
d: dict = {'lines': self.lines.to_dict()}
if self.continuum is not None:
d['continuum'] = self.continuum.to_dict()
if self.dispersers is not None:
d['dispersers'] = self.dispersers.to_dict()
return d
[docs]
@classmethod
def from_dict(cls, d: dict) -> Configuration:
"""Deserialize from a dictionary.
Parameters
----------
d : dict
As produced by :meth:`to_dict`.
Returns
-------
Configuration
"""
from unite.instrument.config import InstrumentConfig
lines = LineConfiguration.from_dict(d['lines'])
continuum = None
if 'continuum' in d:
continuum = ContinuumConfiguration.from_dict(d['continuum'])
dispersers = None
if 'dispersers' in d:
dispersers = InstrumentConfig.from_dict(d['dispersers'])
# Bypass validate() on load — the config was already validated when saved.
obj = cls.__new__(cls)
obj.lines = lines
obj.continuum = continuum
obj.dispersers = dispersers
return obj
# ------------------------------------------------------------------
# YAML serialization
# ------------------------------------------------------------------
[docs]
def to_yaml(self) -> str:
"""Serialize to a YAML string.
Returns
-------
str
"""
return yaml.dump(self.to_dict(), default_flow_style=False, sort_keys=False)
[docs]
@classmethod
def from_yaml(cls, text: str) -> Configuration:
"""Deserialize from a YAML string.
Parameters
----------
text : str
YAML string as produced by :meth:`to_yaml`.
Returns
-------
Configuration
"""
return cls.from_dict(yaml.safe_load(text))
# ------------------------------------------------------------------
# File I/O
# ------------------------------------------------------------------
[docs]
def save(self, path: str | Path) -> None:
"""Save to a YAML file.
Parameters
----------
path : str or Path
Output file path.
"""
Path(path).write_text(self.to_yaml())
[docs]
@classmethod
def load(cls, path: str | Path) -> Configuration:
"""Load from a YAML file.
Parameters
----------
path : str or Path
Path to a YAML file written by :meth:`save`.
Returns
-------
Configuration
"""
return cls.from_yaml(Path(path).read_text())
# ------------------------------------------------------------------
# Dunder
# ------------------------------------------------------------------
def __add__(self, other: Configuration) -> Configuration:
"""Combine two configurations (strict mode — raises on name collisions).
Merges lines, continuum regions, and dispersers from both configurations.
Each sub-config combination follows strict mode — raises if any parameter
or disperser names collide.
Parameters
----------
other : Configuration
Returns
-------
Configuration
New configuration combining lines, continuum, and dispersers from both
*self* and *other*.
Raises
------
ValueError
If any parameter or disperser names appear in both configurations.
TypeError
If *other* is not a :class:`Configuration`.
"""
if not isinstance(other, Configuration):
return NotImplemented
# Combine lines (required, always present)
lines = self.lines + other.lines
# Combine continuum (optional)
continuum = None
if self.continuum is not None and other.continuum is not None:
continuum = self.continuum + other.continuum
elif self.continuum is not None:
continuum = self.continuum
elif other.continuum is not None:
continuum = other.continuum
# Combine dispersers (optional)
dispersers = None
if self.dispersers is not None and other.dispersers is not None:
dispersers = self.dispersers + other.dispersers
elif self.dispersers is not None:
dispersers = self.dispersers
elif other.dispersers is not None:
dispersers = other.dispersers
return Configuration(lines, continuum=continuum, dispersers=dispersers)
def __repr__(self) -> str:
cont_repr = repr(self.continuum) if self.continuum is not None else 'None'
dispersers_repr = (
repr(self.dispersers) if self.dispersers is not None else 'None'
)
return f'Configuration(\n lines={self.lines!r},\n continuum={cont_repr},\n dispersers={dispersers_repr}\n)'