"""Load the master config (config/config.yaml) as a plain dict. The YAML file is the single source of truth for paths, grid, index definition, lead blocks and the cross-validation scheme. """ from __future__ import annotations import os import yaml _DEFAULT = os.path.join( os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "config", "config.yaml", ) def load_config(path: str | None = None) -> dict: with open(path or _DEFAULT, "r") as f: return yaml.safe_load(f) def lead_blocks(cfg: dict) -> dict[str, tuple[int, int]]: """Return {block_name: (day_start, day_end)} inclusive, both ends.""" return {k: (int(v[0]), int(v[1])) for k, v in cfg["lead_blocks"].items()} def block_predictors(cfg: dict, block: str) -> list[str]: """Lead-dependent predictor NAME list for one block: backbone + the block's extras. Falls back to the backbone alone if `predictors` is absent or the block has no entry. Order = backbone first, then the block extras in config order (deduplicated). """ spec = cfg.get("predictors") if not spec: from .features import FEATURE_NAMES return list(FEATURE_NAMES) names = list(spec.get("backbone", [])) for n in spec.get("by_block", {}).get(block, []): if n not in names: names.append(n) return names def block_predictor_index(cfg: dict, block: str, full_names) -> list[int]: """Column indices of the block's lead-dependent predictors into `full_names`. `full_names` is the ordered candidate-matrix column list (e.g. features.FEATURE_NAMES + features_ext.EXT_FEATURE_NAMES + features_ext.GRID_FEATURE_NAMES). Raises if a requested predictor is not present, so a config typo fails loudly. """ idx = {n: i for i, n in enumerate(full_names)} want = block_predictors(cfg, block) missing = [n for n in want if n not in idx] if missing: raise KeyError(f"block {block}: predictors not in candidate matrix: {missing}") return [idx[n] for n in want]