Source code for apem.unit_based_model.evaluation.redispatch_analysis

"""Utilities for loading and validating redispatch cost and volume metrics."""

from __future__ import annotations

from pathlib import Path

import pandas as pd

REQUIRED_COLUMNS = ("redispatch_algorithm", "metric", "value")
SUPPORTED_METRICS = {"costs", "volumes"}

REDISPATCH_FILE_PATTERNS = {
    "costs": "_redispatch_costs.csv",
    "volumes": "_redispatch_vols.csv",
}


[docs] def validate_redispatch_table(df: pd.DataFrame) -> pd.DataFrame: """ Validate and normalize a generic redispatch-analysis input table. :param df: input table expected to contain ``redispatch_algorithm``, ``metric``, and ``value`` :return: normalized copy with lowercase metric labels and numeric ``value`` :raises ValueError: if required columns are missing, metric values are unsupported, algorithm labels are empty, or no numeric values are available """ normalized = df.copy() normalized.columns = [str(column).strip() for column in normalized.columns] missing = [column for column in REQUIRED_COLUMNS if column not in normalized.columns] if missing: raise ValueError(f"Missing required columns: {missing}. Required columns: {list(REQUIRED_COLUMNS)}.") normalized["redispatch_algorithm"] = normalized["redispatch_algorithm"].astype(str).str.strip() normalized["metric"] = normalized["metric"].astype(str).str.strip().str.lower() normalized["value"] = pd.to_numeric(normalized["value"], errors="coerce") if normalized["redispatch_algorithm"].eq("").any(): raise ValueError("Column 'redispatch_algorithm' contains empty values.") invalid_metrics = sorted(set(normalized["metric"]) - SUPPORTED_METRICS) if invalid_metrics: raise ValueError( f"Unsupported metric values: {invalid_metrics}. Supported metrics: {sorted(SUPPORTED_METRICS)}." ) if normalized["value"].notna().sum() == 0: raise ValueError("Column 'value' does not contain any numeric values.") return normalized
[docs] def load_redispatch_metric_file( path: str | Path, *, redispatch_algorithm: str | None = None, metric: str | None = None, ) -> pd.DataFrame: """ Load one redispatch metric file and normalize it to tabular format. The file is expected to contain one ``<label>: <value>`` line. :param path: redispatch metric file path :param redispatch_algorithm: algorithm label override; inferred from the file name when omitted :param metric: metric label override (for example ``costs`` or ``volumes``); inferred from the file name when omitted :return: validated one-row table with ``redispatch_algorithm``, ``metric``, ``value`` :raises ValueError: if parsing fails or inferred values are invalid """ file_path = Path(path) metric_name = metric or _infer_metric_name(file_path) algorithm_name = redispatch_algorithm or _infer_redispatch_algorithm_name(file_path) raw_text = file_path.read_text(encoding="utf-8").strip() if ":" not in raw_text: raise ValueError(f"Could not parse redispatch metric file: {file_path}") _, raw_value = raw_text.split(":", maxsplit=1) df = pd.DataFrame( [ { "redispatch_algorithm": algorithm_name, "metric": metric_name, "value": raw_value.strip(), } ] ) return validate_redispatch_table(df)
def _infer_metric_name(file_path: Path) -> str: name = file_path.name for metric, suffix in REDISPATCH_FILE_PATTERNS.items(): if name.endswith(suffix): return metric raise ValueError(f"Could not infer redispatch metric from file name: {file_path.name}") def _infer_redispatch_algorithm_name(file_path: Path) -> str: name = file_path.name for suffix in REDISPATCH_FILE_PATTERNS.values(): if name.endswith(suffix): return name.removesuffix(suffix).split("_", 1)[0] raise ValueError(f"Could not infer redispatch algorithm from file name: {file_path.name}")