Source code for jesterTOV.inference.config.parser
r"""Configuration file parser for jesterTOV inference system."""
import yaml
from pathlib import Path
from typing import Union
from pydantic import ValidationError
from .schema import InferenceConfig
from jesterTOV.logging_config import get_logger
logger = get_logger("jester")
[docs]
def load_config(config_path: Union[str, Path]) -> InferenceConfig:
"""Load and validate inference configuration from YAML file.
Parameters
----------
config_path : str or Path
Path to YAML configuration file
Returns
-------
InferenceConfig
Validated inference configuration object
Raises
------
FileNotFoundError
If config file does not exist
yaml.YAMLError
If YAML parsing fails
pydantic.ValidationError
If configuration validation fails
Examples
--------
>>> config = load_config("config.yaml")
>>> print(config.eos.type)
metamodel_cse
>>> print(config.tov.type)
gr
"""
config_path = Path(config_path).resolve()
logger.debug(f"Loading configuration from: {config_path}")
if not config_path.exists():
raise FileNotFoundError(f"Configuration file not found: {config_path}")
with open(config_path, "r") as f:
try:
config_dict = yaml.safe_load(f)
except yaml.YAMLError as e:
raise yaml.YAMLError(
f"Error parsing YAML configuration file {config_path}: {e}"
) from e
if config_dict is None:
raise ValueError(f"Configuration file is empty: {config_path}")
logger.debug(f"Raw configuration keys: {list(config_dict.keys())}")
# Resolve relative paths in prior specification file
# Make them relative to the config file directory, not CWD
if "prior" in config_dict and "specification_file" in config_dict["prior"]:
spec_file = Path(config_dict["prior"]["specification_file"])
if not spec_file.is_absolute():
# Resolve relative to config file directory
spec_file = (config_path.parent / spec_file).resolve()
config_dict["prior"]["specification_file"] = str(spec_file)
try:
config = InferenceConfig(**config_dict)
logger.debug("Configuration validation successful")
logger.debug(f" Seed: {config.seed}")
logger.debug(f" EOS type: {config.eos.type}")
logger.debug(f" TOV solver: {config.tov.type}")
logger.debug(f" Prior file: {config.prior.specification_file}")
logger.debug(
f" Enabled likelihoods: {[lk.type for lk in config.likelihoods if lk.enabled]}"
)
logger.debug(f" Sampler type: {config.sampler.type}")
logger.debug(f" Output directory: {config.sampler.output_dir}")
return config
except ValidationError as e:
raise ValueError(_format_validation_error(e, config_path)) from e
except Exception as e:
raise ValueError(
f"Error validating configuration from {config_path}: {e}"
) from e
def _format_validation_error(e: ValidationError, config_path: Path) -> str:
"""Format a Pydantic ValidationError into a clean, actionable message.
Strips Pydantic's auto-appended metadata (``[type=value_error, input_value=...]``)
so that multi-line messages from JesterBaseModel's extra-field validator are
displayed without noise.
"""
_YAML_REFERENCE_URL = "https://nuclear-multimessenger-astronomy.github.io/jester/inference/yaml_reference.html"
blocks: list[str] = [
f"Configuration error in {config_path}:",
]
for error in e.errors():
loc_parts = [str(p) for p in error["loc"] if p != "__root__"]
loc = " → ".join(loc_parts) if loc_parts else "(top level)"
msg: str = error["msg"]
# Pydantic prefixes our ValueError messages with "Value error, " — strip it.
if msg.startswith("Value error, "):
msg = msg[len("Value error, ") :]
blocks.append(f"\n[{loc}]\n{msg}")
blocks.append(f"\nFor all available config options, see: {_YAML_REFERENCE_URL}")
return "\n".join(blocks)