"""1D and 2D parameter sweeps for hardware design exploration.""" from dataclasses import dataclass from typing import List, Optional import numpy as np import pandas as pd from cryosim.calibration.params import ( CALIBRATION_PARAMS, PARAM_NAMES, get_nominal_values, apply_overrides, ) from cryosim.hardware.config import load_config from cryosim.engine.fast import ICV_open @dataclass class SweepResult: """Result of a 1D parameter sweep.""" param_name: str param_values: np.ndarray metric_name: str metric_values: np.ndarray hardware: str Pexit: float speed: float def _extract_metric(out: np.ndarray, hist: dict, metric: str) -> float: """Extract a named metric from engine output.""" if metric == "mdot_kgpm": return hist["mdot_kgpm"] elif metric == "mass_eff": return float(out[1, 0]) elif metric == "Tc_peak_K": return float(np.max(hist["Tc_K"])) elif metric == "kWh_extend": return hist["kWh_extend"] else: return hist.get(metric, np.nan) def sweep_1d( hardware: str, param_name: str, values: List[float], speed: float = 0.65, Pexit: float = 350.0, metric: str = "mdot_kgpm", Ptank: float = 7.0, Psat: float = 2.0, ) -> SweepResult: """Sweep one parameter across a range of values. Args: hardware: Base config name. param_name: One of the 7 calibratable param names. values: List of values to sweep. speed, Pexit: Operating conditions. metric: Which metric to track ("mdot_kgpm", "mass_eff", "Tc_peak_K", "kWh_extend"). Ptank: Inlet tank pressure [barg]. Psat: Saturation pressure [barg]. Returns: SweepResult with param_values and metric_values arrays. """ if param_name not in CALIBRATION_PARAMS: raise ValueError(f"Unknown param '{param_name}'. Available: {list(CALIBRATION_PARAMS.keys())}") cfg = load_config(hardware) nominal = get_nominal_values(hardware) param_idx = PARAM_NAMES.index(param_name) metric_vals = np.zeros(len(values)) for i, val in enumerate(values): params = list(nominal) params[param_idx] = val override_cfg = apply_overrides(cfg, params) args = override_cfg.to_engine_args() try: out, hist = ICV_open( Pexit_barg=Pexit, speed_f=speed, Ptank_barg=Ptank, Psat_barg=Psat, **args, ) metric_vals[i] = _extract_metric(out, hist, metric) except Exception: metric_vals[i] = np.nan return SweepResult( param_name=param_name, param_values=np.array(values), metric_name=metric, metric_values=metric_vals, hardware=hardware, Pexit=Pexit, speed=speed, ) def sweep_2d( hardware: str, param1: str, values1: List[float], param2: str, values2: List[float], speed: float = 0.65, Pexit: float = 350.0, metric: str = "mdot_kgpm", Ptank: float = 7.0, Psat: float = 2.0, ) -> pd.DataFrame: """Sweep two parameters on a 2D grid. Args: hardware: Base config name. param1: First parameter name. values1: Values for first parameter. param2: Second parameter name. values2: Values for second parameter. speed, Pexit: Operating conditions. metric: Which metric to track. Ptank: Inlet tank pressure [barg]. Psat: Saturation pressure [barg]. Returns: DataFrame with columns: param1, param2, metric. """ if param1 not in CALIBRATION_PARAMS or param2 not in CALIBRATION_PARAMS: raise ValueError(f"Unknown params. Available: {list(CALIBRATION_PARAMS.keys())}") cfg = load_config(hardware) nominal = get_nominal_values(hardware) idx1 = PARAM_NAMES.index(param1) idx2 = PARAM_NAMES.index(param2) rows = [] for v1 in values1: for v2 in values2: params = list(nominal) params[idx1] = v1 params[idx2] = v2 override_cfg = apply_overrides(cfg, params) args = override_cfg.to_engine_args() try: out, hist = ICV_open( Pexit_barg=Pexit, speed_f=speed, Ptank_barg=Ptank, Psat_barg=Psat, **args, ) val = _extract_metric(out, hist, metric) except Exception: val = np.nan rows.append({param1: v1, param2: v2, metric: val}) return pd.DataFrame(rows)