Spaces:
Sleeping
Sleeping
| """Hardware dimension optimizer (C4). | |
| Optimizes physical hardware dimensions (bore, port diameter, spring constant, | |
| etc.) that can be handed directly to a machinist, rather than the abstract | |
| calibration knobs used by ``optimizer.py``. | |
| Uses ``scipy.optimize.differential_evolution`` because the physical dimensions | |
| interact nonlinearly and the flow-cliff discontinuity traps gradient methods. | |
| """ | |
| from dataclasses import dataclass, field | |
| from typing import Dict, List, Optional | |
| import numpy as np | |
| from scipy.optimize import differential_evolution | |
| from cryosim.hardware.design_params import ( | |
| HardwareDesignParams, | |
| design_to_engine_config, | |
| ) | |
| from cryosim.engine.fast import ICV_open | |
| from cryosim.optimization.optimizer import ( | |
| _resolve_target, | |
| _parse_constraint, | |
| _check_constraint, | |
| _extract_metrics, | |
| _stall_pressure_with_overrides, | |
| _fill_time_proxy_with_overrides, | |
| ) | |
| class DesignOptimizationResult: | |
| """Result of a hardware-dimension optimization run.""" | |
| optimal_design: HardwareDesignParams | |
| optimal_metrics: Dict[str, float] | |
| target: str | |
| target_value: float | |
| base_hardware: str | |
| n_evals: int | |
| converged: bool | |
| param_changes: Dict[str, tuple] # {name: (before, after, pct_change)} | |
| notes: List[str] = field(default_factory=list) | |
| def __repr__(self) -> str: | |
| changed = sum(1 for _, _, pct in self.param_changes.values() if abs(pct) > 1.0) | |
| return ( | |
| f"DesignOptimizationResult({self.target}={self.target_value:.4f}, " | |
| f"{changed}/{len(self.param_changes)} params changed, " | |
| f"{self.n_evals} evals, converged={self.converged})" | |
| ) | |
| # --------------------------------------------------------------------------- | |
| # Manufacturing notes | |
| # --------------------------------------------------------------------------- | |
| def _manufacturing_notes(design: HardwareDesignParams) -> List[str]: | |
| """Generate practical manufacturing notes for extreme parameter values.""" | |
| notes: List[str] = [] | |
| if design.icv_port_dia_mm < 4.0: | |
| notes.append( | |
| f"ICV port {design.icv_port_dia_mm:.1f} mm may be difficult to " | |
| f"machine and prone to clogging." | |
| ) | |
| if design.dcv_port_dia_mm < 5.0: | |
| notes.append( | |
| f"DCV port {design.dcv_port_dia_mm:.1f} mm is small; verify flow " | |
| f"capacity at target mass flow." | |
| ) | |
| if design.bore_mm < 18.0: | |
| notes.append( | |
| f"Bore {design.bore_mm:.1f} mm is very small; swept volume will " | |
| f"limit throughput." | |
| ) | |
| if design.bore_mm > 45.0: | |
| notes.append( | |
| f"Bore {design.bore_mm:.1f} mm is large; verify seal availability " | |
| f"and structural margin." | |
| ) | |
| if design.dead_volume_frac > 0.06: | |
| notes.append( | |
| f"Dead volume fraction {design.dead_volume_frac:.3f} is high; " | |
| f"consider tighter piston-to-head clearance." | |
| ) | |
| if design.icv_spring_force_N > 40.0: | |
| notes.append( | |
| f"ICV spring preload {design.icv_spring_force_N:.1f} N is high; " | |
| f"may delay ICV opening at low pressures." | |
| ) | |
| if design.dcv_spring_force_N > 15.0: | |
| notes.append( | |
| f"DCV spring preload {design.dcv_spring_force_N:.1f} N is high; " | |
| f"verify DCV closes before backflow at high exit pressures." | |
| ) | |
| return notes | |
| # --------------------------------------------------------------------------- | |
| # Objective builder | |
| # --------------------------------------------------------------------------- | |
| def _build_design_objective( | |
| base_hardware: str, | |
| direction: float, | |
| target_metric: str, | |
| parsed_constraints: Dict[str, tuple], | |
| penalty_weight: float, | |
| speed: float, | |
| Pexit: float, | |
| Ptank: float, | |
| Psat: float, | |
| all_param_names: List[str], | |
| optimized_names: List[str], | |
| fixed_design: HardwareDesignParams, | |
| ): | |
| """Build a closure that maps a design-parameter vector to a scalar objective. | |
| Parameters | |
| ---------- | |
| all_param_names : list[str] | |
| Full ordered list of design parameter names. | |
| optimized_names : list[str] | |
| Subset of names being optimized (others stay at ``fixed_design`` values). | |
| fixed_design : HardwareDesignParams | |
| Baseline design — non-optimized params are taken from here. | |
| """ | |
| n_evals = [0] | |
| def objective(x): | |
| n_evals[0] += 1 | |
| # Build a design from the vector | |
| updates = {name: float(val) for name, val in zip(optimized_names, x)} | |
| design = fixed_design.with_updates(updates) | |
| try: | |
| cfg = design_to_engine_config(design, base_hardware=base_hardware) | |
| except Exception: | |
| return 1e6 | |
| # --- fill-level objectives --- | |
| if target_metric == "stall_pressure": | |
| try: | |
| stall_p = _stall_pressure_with_overrides(cfg, speed, Ptank, Psat) | |
| except Exception: | |
| return 1e6 | |
| obj = direction * stall_p | |
| for cmetric, (op, threshold) in parsed_constraints.items(): | |
| obj += penalty_weight * _check_constraint(0.0, op, threshold) ** 2 | |
| return obj | |
| if target_metric == "fill_time": | |
| try: | |
| proxy = _fill_time_proxy_with_overrides( | |
| cfg, speed, Pexit, Ptank, Psat, | |
| ) | |
| except Exception: | |
| return 1e6 | |
| return direction * proxy | |
| # --- single-cycle objectives --- | |
| args = cfg.to_engine_args() | |
| try: | |
| out, hist = ICV_open( | |
| Pexit_barg=Pexit, speed_f=speed, | |
| Ptank_barg=Ptank, Psat_barg=Psat, **args, | |
| ) | |
| metrics = _extract_metrics(out, hist) | |
| except Exception: | |
| return 1e6 | |
| obj = direction * metrics.get(target_metric, 0.0) | |
| for cmetric, (op, threshold) in parsed_constraints.items(): | |
| violation = _check_constraint(metrics.get(cmetric, 0.0), op, threshold) | |
| obj += penalty_weight * violation ** 2 | |
| return obj | |
| return objective, n_evals | |
| # --------------------------------------------------------------------------- | |
| # Final evaluation | |
| # --------------------------------------------------------------------------- | |
| def _evaluate_design_final( | |
| design: HardwareDesignParams, | |
| base_hardware: str, | |
| speed: float, | |
| Pexit: float, | |
| Ptank: float, | |
| Psat: float, | |
| target_metric: str, | |
| ) -> tuple: | |
| """Run a final engine evaluation and return (metrics_dict, target_value).""" | |
| cfg = design_to_engine_config(design, base_hardware=base_hardware) | |
| if target_metric == "stall_pressure": | |
| try: | |
| stall_p = _stall_pressure_with_overrides(cfg, speed, Ptank, Psat) | |
| return {"stall_pressure": stall_p}, stall_p | |
| except Exception: | |
| return {}, 0.0 | |
| if target_metric == "fill_time": | |
| try: | |
| stall_p = _stall_pressure_with_overrides(cfg, speed, Ptank, Psat) | |
| proxy = -stall_p | |
| return {"fill_time_proxy": proxy, "stall_pressure": stall_p}, proxy | |
| except Exception: | |
| return {}, 0.0 | |
| args = cfg.to_engine_args() | |
| try: | |
| out, hist = ICV_open( | |
| Pexit_barg=Pexit, speed_f=speed, | |
| Ptank_barg=Ptank, Psat_barg=Psat, **args, | |
| ) | |
| metrics = _extract_metrics(out, hist) | |
| except Exception: | |
| metrics = {} | |
| return metrics, metrics.get(target_metric, 0.0) | |
| # --------------------------------------------------------------------------- | |
| # Public API | |
| # --------------------------------------------------------------------------- | |
| def design_optimize( | |
| target: str = "max_stall_pressure", | |
| speed: float = 0.65, | |
| Pexit: float = 500.0, | |
| base_hardware: str = "old_icv", | |
| constraints: Optional[Dict[str, str]] = None, | |
| params_to_optimize: Optional[List[str]] = None, | |
| maxiter: int = 30, | |
| seed: Optional[int] = None, | |
| Ptank: float = 7.0, | |
| Psat: float = 2.0, | |
| penalty_weight: float = 1000.0, | |
| ) -> DesignOptimizationResult: | |
| """Optimize physical hardware dimensions via Differential Evolution. | |
| Unlike ``optimize()`` which searches over 7 abstract calibration knobs, | |
| this function searches over measurable hardware dimensions (bore diameter, | |
| port diameter, spring constant, etc.) that can be handed directly to a | |
| machinist. | |
| Args: | |
| target: Optimization target — "max_mdot", "max_stall_pressure", | |
| "min_kWh", "min_fill_time", etc. | |
| speed: Speed fraction [0-1]. | |
| Pexit: Discharge pressure target [barg] (for single-cycle targets). | |
| base_hardware: Base config name used to fill in non-optimized params. | |
| constraints: e.g. {"Tc_peak_K": "<200", "mass_eff": ">0.1"}. | |
| params_to_optimize: Subset of parameter names to optimize. If None, | |
| all 11 parameters are optimized. | |
| maxiter: Max differential-evolution generations. | |
| seed: Random seed for reproducibility. | |
| Ptank: Inlet tank pressure [barg]. | |
| Psat: Saturation pressure [barg]. | |
| penalty_weight: Penalty multiplier for constraint violations. | |
| Returns: | |
| DesignOptimizationResult with optimal dimensions, metrics, and | |
| manufacturing notes. | |
| """ | |
| constraints = constraints or {} | |
| baseline = HardwareDesignParams() | |
| all_names = baseline.param_names | |
| # Validate params_to_optimize | |
| if params_to_optimize is not None: | |
| unknown = set(params_to_optimize) - set(all_names) | |
| if unknown: | |
| raise ValueError( | |
| f"Unknown design parameters: {unknown}. " | |
| f"Valid names: {all_names}" | |
| ) | |
| opt_names = list(params_to_optimize) | |
| else: | |
| opt_names = list(all_names) | |
| direction, target_metric = _resolve_target(target) | |
| parsed_constraints: Dict[str, tuple] = {} | |
| for cmetric, cstr in constraints.items(): | |
| op, val = _parse_constraint(cstr) | |
| parsed_constraints[cmetric] = (op, val) | |
| # Build bounds for the optimized subset only | |
| opt_bounds = baseline.subset_bounds(opt_names) | |
| objective, n_evals = _build_design_objective( | |
| base_hardware=base_hardware, | |
| direction=direction, | |
| target_metric=target_metric, | |
| parsed_constraints=parsed_constraints, | |
| penalty_weight=penalty_weight, | |
| speed=speed, | |
| Pexit=Pexit, | |
| Ptank=Ptank, | |
| Psat=Psat, | |
| all_param_names=all_names, | |
| optimized_names=opt_names, | |
| fixed_design=baseline, | |
| ) | |
| result = differential_evolution( | |
| objective, | |
| bounds=opt_bounds, | |
| maxiter=maxiter, | |
| seed=seed, | |
| tol=1e-8, | |
| polish=True, | |
| ) | |
| # Reconstruct optimal design | |
| opt_updates = {name: float(val) for name, val in zip(opt_names, result.x)} | |
| optimal_design = baseline.with_updates(opt_updates) | |
| # Final evaluation | |
| final_metrics, target_value = _evaluate_design_final( | |
| optimal_design, base_hardware, speed, Pexit, Ptank, Psat, target_metric, | |
| ) | |
| # Compute per-parameter changes | |
| param_changes: Dict[str, tuple] = {} | |
| for name in all_names: | |
| before = getattr(baseline, name) | |
| after = getattr(optimal_design, name) | |
| if abs(before) > 1e-12: | |
| pct = 100.0 * (after - before) / before | |
| else: | |
| pct = 0.0 if abs(after) < 1e-12 else float("inf") | |
| param_changes[name] = (before, after, pct) | |
| notes = _manufacturing_notes(optimal_design) | |
| return DesignOptimizationResult( | |
| optimal_design=optimal_design, | |
| optimal_metrics=final_metrics, | |
| target=target, | |
| target_value=target_value, | |
| base_hardware=base_hardware, | |
| n_evals=n_evals[0], | |
| converged=result.success, | |
| param_changes=param_changes, | |
| notes=notes, | |
| ) | |