"""CryoSim — Cryogenic Pump Hardware Prototyping Simulator. Quick start: from cryosim import predict result = predict(hardware="old_icv", Pexit=350, speed=0.65) print(result) """ __version__ = "0.1.0" from dataclasses import dataclass from typing import Optional, Dict, Any import numpy as np @dataclass class CycleResult: """Result of a single pump cycle simulation.""" mdot_kgpm: float # Discharge flow rate [kg/min] mass_eff: float # Mass inflow efficiency [0-1] Tc_peak_K: float # Peak chamber temperature [K] pc_peak_barg: float # Peak chamber pressure [barg] DCV_ct_s: float # DCV closure time [s] ICV_open_frac: float # Max ICV opening fraction [0-1] cpu_s: float # Simulation wall time [s] steps: int # Adaptive timesteps used kWh_retract: float # Retract stroke work [kWh/kg] kWh_extend: float # Extend stroke work [kWh/kg] tcycle_s: float # Cycle period [s] hardware_name: str # Config name used Pexit_barg: float # Exit pressure [barg] speed_f: float # Speed fraction [0-1] backflow_warning: bool = False # True if raw mdot was negative (clamped to 0) raw_out: Optional[np.ndarray] = None raw_history: Optional[Dict[str, Any]] = None def __repr__(self) -> str: warn = " BACKFLOW!" if self.backflow_warning else "" return ( f"CycleResult(mdot={self.mdot_kgpm:.4f} kg/min, " f"eff={self.mass_eff:.1%}, " f"Tc_peak={self.Tc_peak_K:.1f} K, " f"pc_peak={self.pc_peak_barg:.1f} barg, " f"steps={self.steps}, " f"cpu={self.cpu_s:.2f}s{warn})" ) def predict( hardware: str, Pexit: float, speed: float, Ptank: float = 7.0, Psat: float = 2.0, flash_eff: float = 0.0, ) -> CycleResult: """Run a single pump cycle simulation. Args: hardware: Config name ("old_icv", "new_icv", "calibrated_lh2") or absolute path to a YAML file. Pexit: Discharge pressure target [barg]. speed: Speed fraction [0-1]. Ptank: Inlet tank pressure [barg]. Default 7.0. Psat: Saturation pressure [barg]. Default 2.0. flash_eff: Thermal mass film boiling efficiency [0-1]. Default 0.0. Returns: CycleResult with all simulation outputs. Raises: ValueError: If inputs are outside valid physical ranges. """ if Pexit < 0: raise ValueError(f"Pexit must be non-negative, got {Pexit}") if not 0.0 < speed <= 1.5: raise ValueError(f"speed must be in (0, 1.5], got {speed}") if Ptank < 0: raise ValueError(f"Ptank must be non-negative, got {Ptank}") from cryosim.hardware.config import load_config from cryosim.engine.fast import ICV_open cfg = load_config(hardware) engine_args = cfg.to_engine_args() out, hist = ICV_open( Pexit_barg=Pexit, speed_f=speed, Ptank_barg=Ptank, Psat_barg=Psat, flash_eff=flash_eff, **engine_args, ) raw_mdot = hist["mdot_kgpm"] _backflow = raw_mdot < 0.0 mdot_clamped = max(0.0, raw_mdot) return CycleResult( mdot_kgpm=mdot_clamped, mass_eff=float(out[1, 0]), Tc_peak_K=float(np.max(hist["Tc_K"])), pc_peak_barg=float(np.max(hist["pc"])), DCV_ct_s=float(out[2, 0]), ICV_open_frac=float(out[4, 0]), cpu_s=float(out[0, 0]), steps=hist["steps"], kWh_retract=hist["kWh_retract"], kWh_extend=hist["kWh_extend"], tcycle_s=hist["tcycle_s"], hardware_name=cfg.name, Pexit_barg=Pexit, speed_f=speed, backflow_warning=_backflow, raw_out=out, raw_history=hist, ) from cryosim.results import FillResult, FillTimeResult from cryosim.optimization.optimizer import ( optimize, multi_start_optimize, differential_evolution_optimize, ) from cryosim.optimization.design_optimizer import design_optimize def fill( hardware: str, target_bar: float, speed: float, tank_volume_L: float = 50.0, max_time_s: float = 1800.0, Ptank: float = 7.0, Psat: float = 2.0, T_inlet_K: float = 55.0, system_loss_kgmin: float = 0.02, **kwargs, ) -> FillResult: """Simulate a complete fill cycle. Args: hardware: Config name ("old_icv", "new_icv", etc.). target_bar: Target discharge pressure [bar]. speed: Speed fraction [0-1]. tank_volume_L: Receiver tank volume [liters]. max_time_s: Maximum simulation time [s]. Ptank: Inlet tank pressure [barg]. Psat: Saturation pressure [barg]. T_inlet_K: Inlet fluid temperature [K]. system_loss_kgmin: Combined system losses (blowby + heat leak + valve leakage) expressed as equivalent mass flow [kg/min]. When net pump flow drops below this threshold the fill is considered stalled, because the pump can no longer overcome parasitic losses. Default 0.02 kg/min. **kwargs: Accepts deprecated ``stall_threshold_kgmin`` for backward compatibility (mapped to ``system_loss_kgmin``). Returns: FillResult with fill profile, stall detection, timing. Raises: ValueError: If inputs are outside valid physical ranges. """ # Backward compatibility: accept old parameter name if "stall_threshold_kgmin" in kwargs: system_loss_kgmin = kwargs.pop("stall_threshold_kgmin") if kwargs: raise TypeError(f"Unexpected keyword arguments: {list(kwargs.keys())}") if not 0.0 < speed <= 1.5: raise ValueError(f"speed must be in (0, 1.5], got {speed}") if Ptank < 0: raise ValueError(f"Ptank must be non-negative, got {Ptank}") if target_bar <= 0: raise ValueError(f"target_bar must be positive, got {target_bar}") if tank_volume_L <= 0: raise ValueError(f"tank_volume_L must be positive, got {tank_volume_L}") from cryosim.fill_sim.simulator import FillSimulator sim = FillSimulator( hardware=hardware, speed=speed, tank_volume_L=tank_volume_L, Ptank=Ptank, Psat=Psat, T_inlet_K=T_inlet_K, system_loss_kgmin=system_loss_kgmin, ) return sim.run(target_bar=target_bar, max_time_s=max_time_s) def stall_pressure( hardware: str, speed: float, Ptank: float = 7.0, Psat: float = 2.0, threshold_kgmin: float = 0.05, ) -> float: """Find the maximum achievable discharge pressure for a hardware config. Uses the pre-computed flow curve to find where flow drops below threshold. Returns: Stall pressure [barg]. """ from cryosim.fill_sim.flow_curve import FlowCurve fc = FlowCurve.build( hardware=hardware, speed=speed, n_points=30, P_min=50.0, P_max=950.0, Ptank=Ptank, Psat=Psat, ) return fc.find_stall(threshold_kgmin=threshold_kgmin) def cliff_pressure( hardware: str, speed: float, Ptank: float = 7.0, Psat: float = 2.0, ) -> Optional["CliffResult"]: """Detect the flow cliff for a hardware config at a given speed. The flow cliff is the narrow pressure band (typically ~5 bar wide) where pump flow drops by 50%+ due to the discharge check valve dynamics. Knowing the cliff location is critical for fill strategy — the pump transitions from bulk filling to trickle-charge above the cliff. Args: hardware: Config name ("old_icv", "new_icv", etc.). speed: Speed fraction [0-1]. Ptank: Inlet tank pressure [barg]. Psat: Saturation pressure [barg]. Returns: CliffResult with cliff boundaries and flow drop info, or None if no significant cliff detected. """ from cryosim.fill_sim.flow_curve import FlowCurve, CliffResult # noqa: F811 fc = FlowCurve.build( hardware=hardware, speed=speed, n_points=30, P_min=50.0, P_max=950.0, Ptank=Ptank, Psat=Psat, adaptive=True, ) return fc.find_cliff() def fill_time( hardware: str, target_bar: float, speed: float, tank_volume_L: float = 50.0, Ptank: float = 7.0, Psat: float = 2.0, T_inlet_K: float = 55.0, ) -> 'FillTimeResult': """Estimate fill time to reach a target pressure. Returns: FillTimeResult with structured info about whether target was reached, and why it wasn't if unreachable. Supports float() for backward compat. """ from cryosim.results import FillTimeResult as _FTR result = fill( hardware=hardware, target_bar=target_bar, speed=speed, tank_volume_L=tank_volume_L, Ptank=Ptank, Psat=Psat, T_inlet_K=T_inlet_K, ) if result.reached_target: return _FTR( time_s=result.fill_time_s, reachable=True, stall_pressure_bar=None, reason="Target reached") elif result.stalled: return _FTR( time_s=None, reachable=False, stall_pressure_bar=result.stall_pressure_bar, reason=f"Pump stalled at {result.stall_pressure_bar:.0f} bar (flow below system loss threshold)") else: return _FTR( time_s=None, reachable=False, stall_pressure_bar=result.peak_pressure_bar, reason=f"Timeout: reached {result.peak_pressure_bar:.0f} bar in {result.fill_time_s:.0f}s") def calibrate( hardware: str, fills: list, maxiter: int = 200, ): """Calibrate engine parameters against measured fill data. Args: hardware: Base hardware config to start from. fills: List of fill dicts, each with keys: Pexit_barg, speed_f, Ptank_barg, Psat_barg, measured_mdot_kgpm maxiter: Maximum L-BFGS-B iterations. Returns: CalibrationResult with fitted params, RMSE, and export methods. """ from cryosim.calibration.engine import Calibrator cal = Calibrator(hardware=hardware, fills=fills, maxiter=maxiter) return cal.run() @dataclass class UncertainPrediction: """Prediction with uncertainty bounds.""" result: CycleResult # The point estimate mdot_bounds: 'UncertaintyBands' # Bounds on mass flow envelope_status: str # VALIDATED/EXTRAPOLATING/OUTSIDE_RANGE def predict_with_uncertainty( hardware: str, Pexit: float, speed: float, Ptank: float = 7.0, Psat: float = 2.0, ) -> UncertainPrediction: """Run prediction with uncertainty bounds. Returns the standard CycleResult plus uncertainty bands computed from calibration residuals and extrapolation penalties. """ from cryosim.validation.uncertainty import ResidualModel, UncertaintyBands from cryosim.validation.envelope import check_envelope result = predict(hardware=hardware, Pexit=Pexit, speed=speed, Ptank=Ptank, Psat=Psat) model = ResidualModel() bounds = model.predict_bounds(result.mdot_kgpm, Pexit, speed) envelope = check_envelope(Pexit, speed) return UncertainPrediction( result=result, mdot_bounds=bounds, envelope_status=envelope.status, ) def sensitivity(hardware="old_icv", speed=0.65, Pexit=350.0, **kwargs): """Run parameter sensitivity analysis. Returns SensitivityResult.""" from cryosim.optimization.sensitivity import run_sensitivity return run_sensitivity(hardware=hardware, speed=speed, Pexit=Pexit, **kwargs) def sweep(hardware, param_name, values, speed=0.65, Pexit=350.0, metric="mdot_kgpm", **kwargs): """Sweep one parameter across values, returning metric at each point.""" from cryosim.optimization.sweep import sweep_1d return sweep_1d(hardware=hardware, param_name=param_name, values=values, speed=speed, Pexit=Pexit, metric=metric, **kwargs) def __getattr__(name): if name == "CliffResult": from cryosim.fill_sim.flow_curve import CliffResult return CliffResult raise AttributeError(f"module 'cryosim' has no attribute {name!r}") __all__ = [ "predict", "CycleResult", "predict_with_uncertainty", "UncertainPrediction", "fill", "stall_pressure", "fill_time", "FillResult", "FillTimeResult", "cliff_pressure", "CliffResult", "calibrate", "sensitivity", "sweep", "optimize", "multi_start_optimize", "differential_evolution_optimize", "design_optimize", "__version__", ]