Spaces:
Sleeping
Sleeping
Sweep/Sim param sync + CSV I/O + HW presets (Old/New ICV)
Browse filesMajor fix for Jimmy's reported bug: Sweep tab now honors Simulation tab baseline params. Cache key expanded to avoid stale results. Added CSV save/load for run params, 1D sweep point labels, and a Hardware dropdown with Baseline / Old ICV / New ICV presets sourced from the ICV sim-driver xlsx.
- murphy_unified/__init__.py +2 -0
- murphy_unified/__main__.py +2 -0
- murphy_unified/agent/config.py +2 -3
- murphy_unified/agent/orchestrator.py +2 -2
- murphy_unified/app.py +108 -0
- murphy_unified/data/.gitkeep +0 -0
- murphy_unified/engine_bridge.py +17 -10
- murphy_unified/engine_params.py +76 -0
- murphy_unified/params_io.py +81 -0
- murphy_unified/process_sim/pump_lut.py +79 -0
- murphy_unified/sim_defaults.py +78 -0
- murphy_unified/sweep_cache.py +21 -1
- murphy_unified/tabs/design_agent.py +1 -1
- murphy_unified/tabs/optimizer.py +176 -0
- murphy_unified/tabs/process_sim.py +87 -3
- murphy_unified/tabs/sensitivity.py +112 -0
- murphy_unified/tabs/simulation.py +175 -112
- murphy_unified/tabs/sweep.py +281 -81
murphy_unified/__init__.py
CHANGED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Murphy Unified β Consolidated Cryogenic Pump Dashboard."""
|
| 2 |
+
__version__ = "0.1.0"
|
murphy_unified/__main__.py
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from murphy_unified.app import main
|
| 2 |
+
main()
|
murphy_unified/agent/config.py
CHANGED
|
@@ -6,15 +6,14 @@ from pathlib import Path
|
|
| 6 |
# ββ Stored IDs (set by scripts/setup_agent.py, loaded from env) ββ
|
| 7 |
|
| 8 |
AGENT_ID = os.environ.get("CSH2_AGENT_ID", "")
|
| 9 |
-
|
| 10 |
-
AGENT_VERSION = int(_agent_version_raw) if _agent_version_raw else 0
|
| 11 |
ENVIRONMENT_ID = os.environ.get("CSH2_ENVIRONMENT_ID", "")
|
| 12 |
|
| 13 |
# ββ Paths ββ
|
| 14 |
|
| 15 |
DATA_DIR = Path(__file__).parent.parent / "data"
|
| 16 |
SENSOR_TAG_MAP_PATH = DATA_DIR / "sensor_tag_map.json"
|
| 17 |
-
DESIGN_RECORDS_DIR = Path(
|
| 18 |
|
| 19 |
# ββ System Prompt ββ
|
| 20 |
|
|
|
|
| 6 |
# ββ Stored IDs (set by scripts/setup_agent.py, loaded from env) ββ
|
| 7 |
|
| 8 |
AGENT_ID = os.environ.get("CSH2_AGENT_ID", "")
|
| 9 |
+
AGENT_VERSION = os.environ.get("CSH2_AGENT_VERSION", "")
|
|
|
|
| 10 |
ENVIRONMENT_ID = os.environ.get("CSH2_ENVIRONMENT_ID", "")
|
| 11 |
|
| 12 |
# ββ Paths ββ
|
| 13 |
|
| 14 |
DATA_DIR = Path(__file__).parent.parent / "data"
|
| 15 |
SENSOR_TAG_MAP_PATH = DATA_DIR / "sensor_tag_map.json"
|
| 16 |
+
DESIGN_RECORDS_DIR = Path(__file__).parent.parent.parent / "design_records"
|
| 17 |
|
| 18 |
# ββ System Prompt ββ
|
| 19 |
|
murphy_unified/agent/orchestrator.py
CHANGED
|
@@ -92,7 +92,7 @@ class DesignSessionOrchestrator:
|
|
| 92 |
resources.append({
|
| 93 |
"type": "file",
|
| 94 |
"file_id": uploaded.id,
|
| 95 |
-
"
|
| 96 |
})
|
| 97 |
log.info("Uploaded sensor tag map: %s", uploaded.id)
|
| 98 |
except Exception: # noqa: BLE001
|
|
@@ -146,7 +146,7 @@ class DesignSessionOrchestrator:
|
|
| 146 |
|
| 147 |
self._emit(SessionEvent(type="status", content="Sending messageβ¦"))
|
| 148 |
|
| 149 |
-
with self._client.beta.sessions.
|
| 150 |
# Send after opening the stream (stream-first pattern)
|
| 151 |
self._client.beta.sessions.events.send(
|
| 152 |
session_id=self.session_id,
|
|
|
|
| 92 |
resources.append({
|
| 93 |
"type": "file",
|
| 94 |
"file_id": uploaded.id,
|
| 95 |
+
"path": "/workspace/sensor_tags.json",
|
| 96 |
})
|
| 97 |
log.info("Uploaded sensor tag map: %s", uploaded.id)
|
| 98 |
except Exception: # noqa: BLE001
|
|
|
|
| 146 |
|
| 147 |
self._emit(SessionEvent(type="status", content="Sending messageβ¦"))
|
| 148 |
|
| 149 |
+
with self._client.beta.sessions.stream(session_id=self.session_id) as stream:
|
| 150 |
# Send after opening the stream (stream-first pattern)
|
| 151 |
self._client.beta.sessions.events.send(
|
| 152 |
session_id=self.session_id,
|
murphy_unified/app.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Murphy Unified β Consolidated Cryogenic Pump Dashboard.
|
| 2 |
+
|
| 3 |
+
Launch: python -m murphy_unified.app
|
| 4 |
+
or: murphy (via pip install entry point)
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import gradio as gr
|
| 8 |
+
|
| 9 |
+
from murphy_unified.sim_defaults import SIM_DEFAULTS
|
| 10 |
+
from murphy_unified.theme import CSS, FONTS_HTML, FORCE_DARK
|
| 11 |
+
from murphy_unified.tabs.simulation import build_simulation_tab
|
| 12 |
+
from murphy_unified.tabs.sweep import build_sweep_tab
|
| 13 |
+
from murphy_unified.tabs.sweep_params import build_sweep_params_tab
|
| 14 |
+
from murphy_unified.tabs.comparison import build_comparison_tab
|
| 15 |
+
from murphy_unified.tabs.real_cycles import build_real_cycles_tab
|
| 16 |
+
from murphy_unified.tabs.design_agent import build_design_agent_tab
|
| 17 |
+
from murphy_unified.tabs.process_sim import build_process_sim_tab
|
| 18 |
+
|
| 19 |
+
# Feature flag: set True to show work-in-progress tabs (Optimizer, Sensitivity)
|
| 20 |
+
SHOW_WIP_TABS = False
|
| 21 |
+
|
| 22 |
+
if SHOW_WIP_TABS:
|
| 23 |
+
from murphy_unified.tabs.optimizer import build_optimizer_tab
|
| 24 |
+
from murphy_unified.tabs.sensitivity import build_sensitivity_tab
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def build_app() -> gr.Blocks:
|
| 28 |
+
with gr.Blocks(title="Murphy Unified") as app:
|
| 29 |
+
app.load(js=FORCE_DARK)
|
| 30 |
+
|
| 31 |
+
# Shared state: Simulation tab writes; Sweep tab reads (baseline panel).
|
| 32 |
+
sim_params_state = gr.State(dict(SIM_DEFAULTS))
|
| 33 |
+
|
| 34 |
+
# Cross-tab bridge: 2D sweep payload flows from Sweep β Process Sim.
|
| 35 |
+
sweep_to_process_state = gr.State(None)
|
| 36 |
+
|
| 37 |
+
gr.HTML(
|
| 38 |
+
'<div style="display:flex;align-items:center;gap:12px;padding:8px 0 4px;">'
|
| 39 |
+
'<span style="font-family:var(--body);font-size:18px;font-weight:700;'
|
| 40 |
+
'letter-spacing:3px;color:var(--accent);text-transform:uppercase;">'
|
| 41 |
+
'Murphy Unified</span>'
|
| 42 |
+
'<span style="font-family:var(--mono);font-size:11px;color:var(--text-dim);">'
|
| 43 |
+
'v0.2.0</span>'
|
| 44 |
+
'<span style="display:inline-flex;align-items:center;gap:6px;'
|
| 45 |
+
'font-family:var(--mono);font-size:10px;color:var(--accent2);'
|
| 46 |
+
'background:rgba(79,201,138,0.07);border:1px solid rgba(79,201,138,0.2);'
|
| 47 |
+
'border-radius:20px;padding:3px 10px;margin-left:auto;">'
|
| 48 |
+
'<span style="width:7px;height:7px;border-radius:50%;background:var(--accent2);'
|
| 49 |
+
'display:inline-block;"></span>READY</span>'
|
| 50 |
+
'</div>'
|
| 51 |
+
)
|
| 52 |
+
|
| 53 |
+
with gr.Tabs():
|
| 54 |
+
with gr.TabItem("Simulation"):
|
| 55 |
+
build_simulation_tab(sim_params_state)
|
| 56 |
+
with gr.TabItem("Sweep"):
|
| 57 |
+
sweep_handles = build_sweep_tab(sim_params_state)
|
| 58 |
+
with gr.TabItem("Sweep Across Parameters"):
|
| 59 |
+
build_sweep_params_tab()
|
| 60 |
+
with gr.TabItem("Process Sim"):
|
| 61 |
+
process_handles = build_process_sim_tab()
|
| 62 |
+
with gr.TabItem("Compare Engines"):
|
| 63 |
+
build_comparison_tab()
|
| 64 |
+
with gr.TabItem("Real Cycles"):
|
| 65 |
+
build_real_cycles_tab()
|
| 66 |
+
with gr.TabItem("Design Agent [WIP]"):
|
| 67 |
+
build_design_agent_tab()
|
| 68 |
+
if SHOW_WIP_TABS:
|
| 69 |
+
with gr.TabItem("Optimizer [WIP]"):
|
| 70 |
+
build_optimizer_tab()
|
| 71 |
+
with gr.TabItem("Sensitivity [WIP]"):
|
| 72 |
+
build_sensitivity_tab()
|
| 73 |
+
|
| 74 |
+
# ββ Wire Sweep tab's Run button to produce the bridge payload ββββ
|
| 75 |
+
sweep_handles["run_btn"].click(
|
| 76 |
+
fn=sweep_handles["run_sweep"],
|
| 77 |
+
inputs=sweep_handles["run_inputs"],
|
| 78 |
+
outputs=[
|
| 79 |
+
sweep_handles["plot_out"],
|
| 80 |
+
sweep_handles["cache_html"],
|
| 81 |
+
sweep_handles["results_df"],
|
| 82 |
+
sweep_to_process_state,
|
| 83 |
+
sweep_handles["send_to_process_btn"],
|
| 84 |
+
],
|
| 85 |
+
)
|
| 86 |
+
|
| 87 |
+
# ββ Wire Send-to-Process-Sim button ββββββββββββββββββββββββββββββ
|
| 88 |
+
sweep_handles["send_to_process_btn"].click(
|
| 89 |
+
fn=process_handles["ingest_fn"],
|
| 90 |
+
inputs=[sweep_to_process_state],
|
| 91 |
+
outputs=[
|
| 92 |
+
process_handles["pump_lut_state"],
|
| 93 |
+
process_handles["pump_lut_status"],
|
| 94 |
+
process_handles["lut_mode"],
|
| 95 |
+
process_handles["sweep_provenance"],
|
| 96 |
+
],
|
| 97 |
+
)
|
| 98 |
+
|
| 99 |
+
return app
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
def main():
|
| 103 |
+
app = build_app()
|
| 104 |
+
app.launch(server_port=7860, css=CSS, head=FONTS_HTML)
|
| 105 |
+
|
| 106 |
+
|
| 107 |
+
if __name__ == "__main__":
|
| 108 |
+
main()
|
murphy_unified/data/.gitkeep
ADDED
|
File without changes
|
murphy_unified/engine_bridge.py
CHANGED
|
@@ -17,16 +17,20 @@ from typing import Any, Dict, Tuple
|
|
| 17 |
|
| 18 |
import numpy as np
|
| 19 |
|
| 20 |
-
# ββ Ensure
|
| 21 |
-
#
|
| 22 |
-
|
| 23 |
-
|
|
|
|
|
|
|
| 24 |
)
|
| 25 |
-
if
|
| 26 |
-
sys.path.insert(0,
|
| 27 |
|
| 28 |
-
#
|
| 29 |
-
_ACCEL_DIR =
|
|
|
|
|
|
|
| 30 |
|
| 31 |
# ββ Euler engine (always available via cryosim) ββββββββββββββββββββββββββββββ
|
| 32 |
from cryosim.engine.fast import ICV_open # noqa: E402
|
|
@@ -84,8 +88,9 @@ def _numba_warmup():
|
|
| 84 |
"""Background warmup for Numba JIT functions.
|
| 85 |
|
| 86 |
Warms up both break_coolprop.h2_props (used by Lightspeed and DP45) and
|
| 87 |
-
the DP45 compiled solver itself
|
| 88 |
-
|
|
|
|
| 89 |
"""
|
| 90 |
global _warmup_ok
|
| 91 |
try:
|
|
@@ -102,6 +107,8 @@ def _numba_warmup():
|
|
| 102 |
finally:
|
| 103 |
_warmup_event.set()
|
| 104 |
|
|
|
|
|
|
|
| 105 |
if _HAS_DP45:
|
| 106 |
try:
|
| 107 |
print("[engine_bridge] DP45 warmup starting...")
|
|
|
|
| 17 |
|
| 18 |
import numpy as np
|
| 19 |
|
| 20 |
+
# ββ Ensure ode_reformulation is on sys.path ββββββββββββββββββββββββββββββββββ
|
| 21 |
+
# __file__ = .../CSH2/murphy-unified/murphy_unified/engine_bridge.py
|
| 22 |
+
# Two parents up reaches CSH2/
|
| 23 |
+
_ODE_DIR = os.path.normpath(
|
| 24 |
+
os.path.join(os.path.dirname(__file__), os.pardir, os.pardir,
|
| 25 |
+
"cycle2mdot_acceleration", "ode_reformulation")
|
| 26 |
)
|
| 27 |
+
if _ODE_DIR not in sys.path:
|
| 28 |
+
sys.path.insert(0, _ODE_DIR)
|
| 29 |
|
| 30 |
+
# Also add the parent of ode_reformulation so break_coolprop is importable
|
| 31 |
+
_ACCEL_DIR = os.path.dirname(_ODE_DIR)
|
| 32 |
+
if _ACCEL_DIR not in sys.path:
|
| 33 |
+
sys.path.insert(0, _ACCEL_DIR)
|
| 34 |
|
| 35 |
# ββ Euler engine (always available via cryosim) ββββββββββββββββββββββββββββββ
|
| 36 |
from cryosim.engine.fast import ICV_open # noqa: E402
|
|
|
|
| 88 |
"""Background warmup for Numba JIT functions.
|
| 89 |
|
| 90 |
Warms up both break_coolprop.h2_props (used by Lightspeed and DP45) and
|
| 91 |
+
the DP45 compiled solver itself (solve_cycle_compiled). The DP45 warmup
|
| 92 |
+
runs a tiny cycle so the first user-facing H2/general_ode click doesn't
|
| 93 |
+
pay the ~30β60s JIT compilation cost.
|
| 94 |
"""
|
| 95 |
global _warmup_ok
|
| 96 |
try:
|
|
|
|
| 107 |
finally:
|
| 108 |
_warmup_event.set()
|
| 109 |
|
| 110 |
+
# Pre-compile DP45 kernel (does not gate _warmup_ok β Lightspeed must
|
| 111 |
+
# still be declared ready even if DP45 warmup fails)
|
| 112 |
if _HAS_DP45:
|
| 113 |
try:
|
| 114 |
print("[engine_bridge] DP45 warmup starting...")
|
murphy_unified/engine_params.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Pack a flat Simulation-parameter dict into cycle_sim keyword arguments.
|
| 2 |
+
|
| 3 |
+
Shared by `run_simulation` (Tab 1) and `_sim_point` (Tab 2 sweep) so both
|
| 4 |
+
tabs always hand identical kwargs to `cycle_sim`. If either tab wants a
|
| 5 |
+
swept/override value, it merges `{key: value}` into the input dict BEFORE
|
| 6 |
+
calling this helper.
|
| 7 |
+
|
| 8 |
+
The array ordering here MUST match what `murphy_unified/engine_bridge.py`
|
| 9 |
+
and the underlying engines expect. See `tabs/simulation.py` (pre-refactor)
|
| 10 |
+
lines ~237-258 for the original packing.
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
from __future__ import annotations
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def build_engine_kwargs(params: dict) -> dict:
|
| 17 |
+
"""Convert a flat params dict into kwargs for ``cycle_sim``.
|
| 18 |
+
|
| 19 |
+
Parameters
|
| 20 |
+
----------
|
| 21 |
+
params : dict
|
| 22 |
+
Flat dict keyed by `run_simulation` parameter names (see
|
| 23 |
+
`murphy_unified/sim_defaults.py::SIM_DEFAULTS`).
|
| 24 |
+
|
| 25 |
+
Returns
|
| 26 |
+
-------
|
| 27 |
+
dict with keys:
|
| 28 |
+
``Pexit_barg``, ``speed_f``, ``Ptank_barg``, ``Psat_barg``,
|
| 29 |
+
``ICVparam``, ``DCVparam``, ``pump_geom``, ``proc_param``,
|
| 30 |
+
``flash_eff``, and optionally ``exit_param`` and ``n_cycles``.
|
| 31 |
+
"""
|
| 32 |
+
ICVparam = [
|
| 33 |
+
params["icv_port"], params["icv_mass"], params["icv_travel"],
|
| 34 |
+
params["icv_dp_area"], params["icv_Fs"], params["icv_SC"],
|
| 35 |
+
params["icv_leakKv"], params["icv_comp_eff"], int(params["icv_npts"]),
|
| 36 |
+
]
|
| 37 |
+
DCVparam = [
|
| 38 |
+
params["dcv_port"], params["dcv_mass"], params["dcv_travel"],
|
| 39 |
+
params["dcv_dp_area"], params["dcv_Fs"], params["dcv_SC"],
|
| 40 |
+
params["dcv_leakKv"], params["dcv_comp_eff"], int(params["dcv_npts"]),
|
| 41 |
+
]
|
| 42 |
+
pump_geom = [
|
| 43 |
+
params["bore"], params["stroke"], params["hOD"], params["cLen"],
|
| 44 |
+
params["emH"], params["emS"], params["kvoid"], params["kH"],
|
| 45 |
+
params["Vfv"], params["vac"], params["cpm"], params["dvf"],
|
| 46 |
+
]
|
| 47 |
+
proc_param = [
|
| 48 |
+
params["Tamb"], params["htc"], params["drive"], params["Fmult"],
|
| 49 |
+
params["KvBB"], params["Pbb"], params["fric"], params["Exp"],
|
| 50 |
+
]
|
| 51 |
+
|
| 52 |
+
kw: dict = {
|
| 53 |
+
"Pexit_barg": params["Pexit"],
|
| 54 |
+
"speed_f": params["speed"],
|
| 55 |
+
"Ptank_barg": params["Ptank"],
|
| 56 |
+
"Psat_barg": params["Psat"],
|
| 57 |
+
"ICVparam": ICVparam,
|
| 58 |
+
"DCVparam": DCVparam,
|
| 59 |
+
"pump_geom": pump_geom,
|
| 60 |
+
"proc_param": proc_param,
|
| 61 |
+
"flash_eff": params["flash_eff"],
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
fill = params.get("fill_type", "Fixed Pexit")
|
| 65 |
+
if fill == "CHSS Fill":
|
| 66 |
+
kw["exit_param"] = [1, params["vsnubber"], params["vchss"],
|
| 67 |
+
params["aov140f"], params["rodia"]]
|
| 68 |
+
elif fill == "RO Vent":
|
| 69 |
+
kw["exit_param"] = [0, params["vsnubber"], 0,
|
| 70 |
+
params["aov140f"], params["rodia"]]
|
| 71 |
+
|
| 72 |
+
n_cyc = int(params.get("num_cycles", 1) or 1)
|
| 73 |
+
if n_cyc > 1:
|
| 74 |
+
kw["n_cycles"] = n_cyc
|
| 75 |
+
|
| 76 |
+
return kw
|
murphy_unified/params_io.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""CSV round-trip for Simulation-tab run parameters.
|
| 2 |
+
|
| 3 |
+
Format
|
| 4 |
+
------
|
| 5 |
+
Two-column CSV (``key,value``) written with ``csv.writer`` (QUOTE_MINIMAL).
|
| 6 |
+
One row per entry in :data:`murphy_unified.sim_defaults.SIM_DEFAULTS`.
|
| 7 |
+
|
| 8 |
+
Encoding rules (documented in
|
| 9 |
+
``docs/superpowers/specs/2026-04-14-sweep-sim-sync-and-export.md``):
|
| 10 |
+
* floats/ints β ``repr()`` then ``float()`` on read (bit-exact)
|
| 11 |
+
* strings β as-is; csv.writer quotes automatically when needed
|
| 12 |
+
* lists[str] β ``|``-joined (e.g. ``Energy Balance|Chamber Density``)
|
| 13 |
+
* empty numeric β fall back to ``SIM_DEFAULTS[key]`` on read
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
from __future__ import annotations
|
| 17 |
+
|
| 18 |
+
import csv
|
| 19 |
+
import io
|
| 20 |
+
|
| 21 |
+
from murphy_unified.sim_defaults import SIM_DEFAULTS
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
_LIST_KEYS = {"extra_plots"}
|
| 25 |
+
_STRING_KEYS = {"engine_name", "fluid", "fill_type"}
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def params_to_csv(params: dict) -> str:
|
| 29 |
+
"""Serialize a params dict to CSV text."""
|
| 30 |
+
buf = io.StringIO()
|
| 31 |
+
writer = csv.writer(buf)
|
| 32 |
+
writer.writerow(["key", "value"])
|
| 33 |
+
for key in SIM_DEFAULTS:
|
| 34 |
+
if key not in params:
|
| 35 |
+
continue
|
| 36 |
+
val = params[key]
|
| 37 |
+
if key in _LIST_KEYS:
|
| 38 |
+
encoded = "|".join(val) if val else ""
|
| 39 |
+
elif key in _STRING_KEYS:
|
| 40 |
+
encoded = str(val)
|
| 41 |
+
else:
|
| 42 |
+
encoded = repr(val)
|
| 43 |
+
writer.writerow([key, encoded])
|
| 44 |
+
return buf.getvalue()
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def csv_to_params(csv_text: str) -> tuple[dict, list[str]]:
|
| 48 |
+
"""Parse CSV text into a params dict.
|
| 49 |
+
|
| 50 |
+
Returns
|
| 51 |
+
-------
|
| 52 |
+
(params, skipped_keys)
|
| 53 |
+
``params`` contains successfully-parsed entries; ``skipped_keys``
|
| 54 |
+
lists unknown keys so the UI can surface them.
|
| 55 |
+
"""
|
| 56 |
+
reader = csv.reader(io.StringIO(csv_text))
|
| 57 |
+
rows = list(reader)
|
| 58 |
+
if not rows:
|
| 59 |
+
return {}, []
|
| 60 |
+
if rows[0] == ["key", "value"]:
|
| 61 |
+
rows = rows[1:]
|
| 62 |
+
|
| 63 |
+
parsed: dict = {}
|
| 64 |
+
skipped: list[str] = []
|
| 65 |
+
for row in rows:
|
| 66 |
+
if len(row) != 2:
|
| 67 |
+
continue
|
| 68 |
+
key, value = row
|
| 69 |
+
if key not in SIM_DEFAULTS:
|
| 70 |
+
skipped.append(key)
|
| 71 |
+
continue
|
| 72 |
+
if key in _LIST_KEYS:
|
| 73 |
+
parsed[key] = value.split("|") if value else []
|
| 74 |
+
elif key in _STRING_KEYS:
|
| 75 |
+
parsed[key] = value
|
| 76 |
+
else:
|
| 77 |
+
if value == "":
|
| 78 |
+
parsed[key] = SIM_DEFAULTS[key]
|
| 79 |
+
else:
|
| 80 |
+
parsed[key] = float(value)
|
| 81 |
+
return parsed, skipped
|
murphy_unified/process_sim/pump_lut.py
CHANGED
|
@@ -138,6 +138,85 @@ class PumpLUT:
|
|
| 138 |
|
| 139 |
return cls(speeds, pressures, mdot_grid, temp_grid)
|
| 140 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
def lookup(self, speed_pct: float, P_back_bar: float) -> tuple[float, float]:
|
| 142 |
"""Return (mdot_kg_s, T_discharge_K) at given speed and backpressure."""
|
| 143 |
s = float(np.clip(speed_pct, self.speeds[0], self.speeds[-1]))
|
|
|
|
| 138 |
|
| 139 |
return cls(speeds, pressures, mdot_grid, temp_grid)
|
| 140 |
|
| 141 |
+
@classmethod
|
| 142 |
+
def from_sweep_grid(
|
| 143 |
+
cls,
|
| 144 |
+
x_vals: "np.ndarray",
|
| 145 |
+
y_vals: "np.ndarray",
|
| 146 |
+
z_mdot_kgpm: "np.ndarray",
|
| 147 |
+
z_temp_K: "np.ndarray",
|
| 148 |
+
axis_x: str,
|
| 149 |
+
axis_y: str,
|
| 150 |
+
) -> "PumpLUT":
|
| 151 |
+
"""Build a PumpLUT from a 2D sweep payload on (Pexit_barg, speed_f).
|
| 152 |
+
|
| 153 |
+
Handles unit conversions and axis normalization.
|
| 154 |
+
|
| 155 |
+
Parameters
|
| 156 |
+
----------
|
| 157 |
+
x_vals, y_vals : np.ndarray
|
| 158 |
+
1D arrays of the sweep's X and Y axis values (in sweep-native units).
|
| 159 |
+
z_mdot_kgpm, z_temp_K : np.ndarray
|
| 160 |
+
2D grids of shape ``(len(y_vals), len(x_vals))`` β mass flow in
|
| 161 |
+
kg/min and peak chamber temperature in Kelvin.
|
| 162 |
+
axis_x, axis_y : str
|
| 163 |
+
Axis names; must be exactly the set {"Pexit_barg", "speed_f"}.
|
| 164 |
+
|
| 165 |
+
Returns
|
| 166 |
+
-------
|
| 167 |
+
PumpLUT
|
| 168 |
+
LUT with ``speeds`` as axis 0 (motor_pct β [0, 100]) and
|
| 169 |
+
``pressures`` as axis 1 (absolute bar).
|
| 170 |
+
|
| 171 |
+
Notes
|
| 172 |
+
-----
|
| 173 |
+
Conversions:
|
| 174 |
+
* speed_f [0, 1] β motor_pct [0, 100] (Γ 100)
|
| 175 |
+
* Pexit_barg β back_pressure_bar (+ 1.01325)
|
| 176 |
+
* mdot_kgpm β mdot_kgs (Γ· 60)
|
| 177 |
+
* Tc_peak_K β T_discharge_K (identity)
|
| 178 |
+
"""
|
| 179 |
+
valid_pair = {"Pexit_barg", "speed_f"}
|
| 180 |
+
if {axis_x, axis_y} != valid_pair:
|
| 181 |
+
raise ValueError(
|
| 182 |
+
f"from_sweep_grid requires axes {valid_pair}; got "
|
| 183 |
+
f"{{'{axis_x}', '{axis_y}'}}"
|
| 184 |
+
)
|
| 185 |
+
|
| 186 |
+
import numpy as _np
|
| 187 |
+
|
| 188 |
+
# Normalize so `speed_vals` is along axis 0 and `pexit_vals` is axis 1.
|
| 189 |
+
# Convention: z arrays have shape (len(y_vals), len(x_vals)).
|
| 190 |
+
if axis_x == "speed_f":
|
| 191 |
+
# x=speed_f, y=Pexit_barg β z shape is (Npexit, Nspeed)
|
| 192 |
+
# Transpose to (Nspeed, Npexit) for PumpLUT orientation.
|
| 193 |
+
speed_vals = _np.asarray(x_vals, dtype=float)
|
| 194 |
+
pexit_vals = _np.asarray(y_vals, dtype=float)
|
| 195 |
+
mdot_sp = _np.asarray(z_mdot_kgpm, dtype=float).T # (speeds, pressures)
|
| 196 |
+
temp_sp = _np.asarray(z_temp_K, dtype=float).T
|
| 197 |
+
else:
|
| 198 |
+
# axis_x == "Pexit_barg", axis_y == "speed_f"
|
| 199 |
+
# x=Pexit_barg, y=speed_f β z shape is (Nspeed, Npexit) β correct.
|
| 200 |
+
speed_vals = _np.asarray(y_vals, dtype=float)
|
| 201 |
+
pexit_vals = _np.asarray(x_vals, dtype=float)
|
| 202 |
+
mdot_sp = _np.asarray(z_mdot_kgpm, dtype=float)
|
| 203 |
+
temp_sp = _np.asarray(z_temp_K, dtype=float)
|
| 204 |
+
|
| 205 |
+
speeds = speed_vals * 100.0
|
| 206 |
+
pressures = pexit_vals + 1.01325
|
| 207 |
+
mdot_grid = mdot_sp / 60.0 # kg/min β kg/s
|
| 208 |
+
temp_grid = temp_sp # identity
|
| 209 |
+
|
| 210 |
+
# Sort both axes ascending (PumpLUT/RegularGridInterpolator requires this).
|
| 211 |
+
s_order = _np.argsort(speeds)
|
| 212 |
+
p_order = _np.argsort(pressures)
|
| 213 |
+
speeds = speeds[s_order]
|
| 214 |
+
pressures = pressures[p_order]
|
| 215 |
+
mdot_grid = mdot_grid[s_order][:, p_order]
|
| 216 |
+
temp_grid = temp_grid[s_order][:, p_order]
|
| 217 |
+
|
| 218 |
+
return cls(speeds, pressures, mdot_grid, temp_grid)
|
| 219 |
+
|
| 220 |
def lookup(self, speed_pct: float, P_back_bar: float) -> tuple[float, float]:
|
| 221 |
"""Return (mdot_kg_s, T_discharge_K) at given speed and backpressure."""
|
| 222 |
s = float(np.clip(speed_pct, self.speeds[0], self.speeds[-1]))
|
murphy_unified/sim_defaults.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Single canonical source of Simulation-tab parameter defaults.
|
| 2 |
+
|
| 3 |
+
All Simulation-tab widgets, the initial `sim_params_state`, and the CSV I/O
|
| 4 |
+
key set derive from this dict. Keeping one source prevents drift between
|
| 5 |
+
Simulation widget defaults, Sweep-tab defaults, and engine defaults.
|
| 6 |
+
|
| 7 |
+
Keys match the `run_simulation` signature (murphy_unified/tabs/simulation.py).
|
| 8 |
+
"""
|
| 9 |
+
|
| 10 |
+
from __future__ import annotations
|
| 11 |
+
|
| 12 |
+
SIM_DEFAULTS: dict = {
|
| 13 |
+
# ββ Operating βββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 14 |
+
"engine_name": "Lightspeed",
|
| 15 |
+
"fluid": "H2",
|
| 16 |
+
"Pexit": 350.0,
|
| 17 |
+
"speed": 0.65,
|
| 18 |
+
"Ptank": 7.0,
|
| 19 |
+
"Psat": 2.0,
|
| 20 |
+
|
| 21 |
+
# ββ ICV (9) βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 22 |
+
"icv_port": 20.5,
|
| 23 |
+
"icv_mass": 48.0,
|
| 24 |
+
"icv_travel": 5.0,
|
| 25 |
+
"icv_dp_area": 779.3,
|
| 26 |
+
"icv_Fs": 33.4,
|
| 27 |
+
"icv_SC": 3.75,
|
| 28 |
+
"icv_leakKv": 7.36e-5,
|
| 29 |
+
"icv_comp_eff": 1.0,
|
| 30 |
+
"icv_npts": 100,
|
| 31 |
+
|
| 32 |
+
# ββ DCV (9) βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 33 |
+
"dcv_port": 8.3,
|
| 34 |
+
"dcv_mass": 25.0,
|
| 35 |
+
"dcv_travel": 3.79,
|
| 36 |
+
"dcv_dp_area": 78.54,
|
| 37 |
+
"dcv_Fs": 7.8,
|
| 38 |
+
"dcv_SC": 1.053,
|
| 39 |
+
"dcv_leakKv": 2.6e-6,
|
| 40 |
+
"dcv_comp_eff": 0.8,
|
| 41 |
+
"dcv_npts": 200,
|
| 42 |
+
|
| 43 |
+
# ββ Pump geometry (12) ββββββββββββββββββββββββββββββββββββββββββββ
|
| 44 |
+
"bore": 40.3,
|
| 45 |
+
"stroke": 60.0,
|
| 46 |
+
"hOD": 120.0,
|
| 47 |
+
"cLen": 300.0,
|
| 48 |
+
"emH": 0.8,
|
| 49 |
+
"emS": 0.8,
|
| 50 |
+
"kvoid": 0.026,
|
| 51 |
+
"kH": 15.0,
|
| 52 |
+
"Vfv": 37.0,
|
| 53 |
+
"vac": 760000.0,
|
| 54 |
+
"cpm": 500.0,
|
| 55 |
+
"dvf": 0.01,
|
| 56 |
+
|
| 57 |
+
# ββ Process / losses (9) ββββββββββββββββββββββββββββββββββββββββββ
|
| 58 |
+
"Tamb": 300.0,
|
| 59 |
+
"htc": 10.0,
|
| 60 |
+
"drive": 4.0,
|
| 61 |
+
"Fmult": 30.0,
|
| 62 |
+
"KvBB": 0.006,
|
| 63 |
+
"Pbb": 0.0,
|
| 64 |
+
"fric": 0.5,
|
| 65 |
+
"Exp": 0.2,
|
| 66 |
+
"flash_eff": 0.02,
|
| 67 |
+
|
| 68 |
+
# ββ Downstream / fill (6) βββββββββββββββββββββββββββββββββββββββββ
|
| 69 |
+
"fill_type": "Fixed Pexit",
|
| 70 |
+
"num_cycles": 1,
|
| 71 |
+
"vsnubber": 5.0,
|
| 72 |
+
"vchss": 50.0,
|
| 73 |
+
"aov140f": 1.0,
|
| 74 |
+
"rodia": 0.94,
|
| 75 |
+
|
| 76 |
+
# ββ Output selector βββββββββββββββββββββββββββββββββββββββββββββββ
|
| 77 |
+
"extra_plots": ["Energy Balance"],
|
| 78 |
+
}
|
murphy_unified/sweep_cache.py
CHANGED
|
@@ -8,15 +8,26 @@ import sqlite3
|
|
| 8 |
from typing import Any, Dict, Optional, Tuple
|
| 9 |
|
| 10 |
|
|
|
|
|
|
|
|
|
|
| 11 |
class SweepCache:
|
| 12 |
"""Stores simulation scalar results in a local SQLite database.
|
| 13 |
|
| 14 |
Keys are derived from a SHA256 hash of the canonicalised parameter dict,
|
| 15 |
so identical runs are served from cache instead of re-simulated.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
"""
|
| 17 |
|
| 18 |
def __init__(self, db_path: str = "sweep_cache.db") -> None:
|
| 19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
self._conn.execute("PRAGMA journal_mode=WAL")
|
| 21 |
self._conn.execute(
|
| 22 |
"CREATE TABLE IF NOT EXISTS cache ("
|
|
@@ -26,10 +37,19 @@ class SweepCache:
|
|
| 26 |
" ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP"
|
| 27 |
")"
|
| 28 |
)
|
|
|
|
| 29 |
self._conn.commit()
|
| 30 |
self._session_hits = 0
|
| 31 |
self._session_misses = 0
|
| 32 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
# ------------------------------------------------------------------
|
| 34 |
# Internal helpers
|
| 35 |
# ------------------------------------------------------------------
|
|
|
|
| 8 |
from typing import Any, Dict, Optional, Tuple
|
| 9 |
|
| 10 |
|
| 11 |
+
SCHEMA_VERSION = 3
|
| 12 |
+
|
| 13 |
+
|
| 14 |
class SweepCache:
|
| 15 |
"""Stores simulation scalar results in a local SQLite database.
|
| 16 |
|
| 17 |
Keys are derived from a SHA256 hash of the canonicalised parameter dict,
|
| 18 |
so identical runs are served from cache instead of re-simulated.
|
| 19 |
+
|
| 20 |
+
Schema is versioned via ``PRAGMA user_version``. On open, if the stored
|
| 21 |
+
version differs from ``SCHEMA_VERSION``, all rows are dropped and the
|
| 22 |
+
version is written.
|
| 23 |
"""
|
| 24 |
|
| 25 |
def __init__(self, db_path: str = "sweep_cache.db") -> None:
|
| 26 |
+
# check_same_thread=False: Gradio serves requests from a worker pool,
|
| 27 |
+
# so the same SweepCache instance is used across threads. Access is
|
| 28 |
+
# serialized at the Python level by SQLite's own locking, so cross-
|
| 29 |
+
# thread use is safe in practice.
|
| 30 |
+
self._conn = sqlite3.connect(db_path, check_same_thread=False)
|
| 31 |
self._conn.execute("PRAGMA journal_mode=WAL")
|
| 32 |
self._conn.execute(
|
| 33 |
"CREATE TABLE IF NOT EXISTS cache ("
|
|
|
|
| 37 |
" ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP"
|
| 38 |
")"
|
| 39 |
)
|
| 40 |
+
self._migrate_if_needed()
|
| 41 |
self._conn.commit()
|
| 42 |
self._session_hits = 0
|
| 43 |
self._session_misses = 0
|
| 44 |
|
| 45 |
+
def _migrate_if_needed(self) -> None:
|
| 46 |
+
"""If stored schema version != SCHEMA_VERSION, drop all rows."""
|
| 47 |
+
cur = self._conn.execute("PRAGMA user_version")
|
| 48 |
+
current = cur.fetchone()[0]
|
| 49 |
+
if current != SCHEMA_VERSION:
|
| 50 |
+
self._conn.execute("DELETE FROM cache")
|
| 51 |
+
self._conn.execute(f"PRAGMA user_version = {SCHEMA_VERSION}")
|
| 52 |
+
|
| 53 |
# ------------------------------------------------------------------
|
| 54 |
# Internal helpers
|
| 55 |
# ------------------------------------------------------------------
|
murphy_unified/tabs/design_agent.py
CHANGED
|
@@ -103,7 +103,7 @@ def _end_session():
|
|
| 103 |
def _load_session_history():
|
| 104 |
records = list_design_records(DESIGN_RECORDS_DIR)
|
| 105 |
choices = [r["slug"] for r in records]
|
| 106 |
-
return gr.
|
| 107 |
|
| 108 |
|
| 109 |
def _load_record(slug):
|
|
|
|
| 103 |
def _load_session_history():
|
| 104 |
records = list_design_records(DESIGN_RECORDS_DIR)
|
| 105 |
choices = [r["slug"] for r in records]
|
| 106 |
+
return gr.update(choices=choices, value=choices[0] if choices else None)
|
| 107 |
|
| 108 |
|
| 109 |
def _load_record(slug):
|
murphy_unified/tabs/optimizer.py
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Tab 3 -- Constraint-aware hardware optimizer via cryosim.
|
| 2 |
+
|
| 3 |
+
Builds all Gradio components (left control panel + right results panel)
|
| 4 |
+
and wires up the click handler. Must be called inside an active
|
| 5 |
+
``gr.TabItem`` context.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import traceback
|
| 11 |
+
|
| 12 |
+
import gradio as gr
|
| 13 |
+
import pandas as pd
|
| 14 |
+
|
| 15 |
+
import cryosim
|
| 16 |
+
from cryosim.calibration.params import PARAM_NAMES, get_nominal_values
|
| 17 |
+
from murphy_unified.theme import metric_html, TRACE_COLORS, ACCENT, TEXT_DIM, engine_banner
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
# -- Click handler -----------------------------------------------------------
|
| 21 |
+
|
| 22 |
+
def run_optimize(
|
| 23 |
+
hardware: str,
|
| 24 |
+
target: str,
|
| 25 |
+
Pexit: float,
|
| 26 |
+
speed: float,
|
| 27 |
+
maxiter: float,
|
| 28 |
+
c_metric: str,
|
| 29 |
+
c_op: str,
|
| 30 |
+
c_val: float,
|
| 31 |
+
):
|
| 32 |
+
"""Run constrained optimisation and return (metrics_html, table_df)."""
|
| 33 |
+
|
| 34 |
+
try:
|
| 35 |
+
# Build constraints dict
|
| 36 |
+
constraints: dict[str, str] = {}
|
| 37 |
+
if c_metric:
|
| 38 |
+
constraints[c_metric] = f"{c_op}{c_val}"
|
| 39 |
+
|
| 40 |
+
# Baseline prediction
|
| 41 |
+
baseline = cryosim.predict(hardware=hardware, Pexit=Pexit, speed=speed)
|
| 42 |
+
|
| 43 |
+
# Optimise
|
| 44 |
+
opt = cryosim.optimize(
|
| 45 |
+
hardware=hardware,
|
| 46 |
+
target=target,
|
| 47 |
+
speed=speed,
|
| 48 |
+
Pexit=Pexit,
|
| 49 |
+
constraints=constraints,
|
| 50 |
+
maxiter=int(maxiter),
|
| 51 |
+
)
|
| 52 |
+
|
| 53 |
+
# Parameter comparison table
|
| 54 |
+
nominal = get_nominal_values(hardware)
|
| 55 |
+
rows = []
|
| 56 |
+
for name, nom, opt_val in zip(PARAM_NAMES, nominal, opt.optimal_values):
|
| 57 |
+
pct = (opt_val / nom - 1) * 100 if abs(nom) > 1e-12 else 0
|
| 58 |
+
rows.append({
|
| 59 |
+
"Parameter": name,
|
| 60 |
+
"Nominal": f"{nom:.6f}",
|
| 61 |
+
"Optimized": f"{opt_val:.6f}",
|
| 62 |
+
"Change %": f"{pct:+.1f}%",
|
| 63 |
+
})
|
| 64 |
+
table_df = pd.DataFrame(rows)
|
| 65 |
+
|
| 66 |
+
# Metric cards
|
| 67 |
+
status_accent = "green" if opt.constraints_satisfied else "red"
|
| 68 |
+
baseline_val = getattr(baseline, "mdot_kgpm", 0) if "mdot" in target else 0
|
| 69 |
+
opt_val_scalar = opt.target_value
|
| 70 |
+
improvement = (
|
| 71 |
+
((opt_val_scalar / baseline_val - 1) * 100)
|
| 72 |
+
if baseline_val != 0
|
| 73 |
+
else 0
|
| 74 |
+
)
|
| 75 |
+
|
| 76 |
+
metrics = (
|
| 77 |
+
'<div class="metric-row">'
|
| 78 |
+
+ metric_html("BASELINE", f"{baseline_val:.4f}")
|
| 79 |
+
+ metric_html("OPTIMIZED", f"{opt_val_scalar:.4f}", "", "green")
|
| 80 |
+
+ metric_html(
|
| 81 |
+
"IMPROVEMENT",
|
| 82 |
+
f"{improvement:+.1f}",
|
| 83 |
+
"%",
|
| 84 |
+
"green" if improvement > 0 else "amber",
|
| 85 |
+
)
|
| 86 |
+
+ metric_html(
|
| 87 |
+
"CONSTRAINTS",
|
| 88 |
+
"OK" if opt.constraints_satisfied else "VIOLATED",
|
| 89 |
+
"",
|
| 90 |
+
status_accent,
|
| 91 |
+
)
|
| 92 |
+
+ "</div>"
|
| 93 |
+
+ '<div class="metric-row">'
|
| 94 |
+
+ metric_html("EVALUATIONS", f"{opt.n_evals}")
|
| 95 |
+
+ metric_html("CONVERGED", "YES" if opt.converged else "NO")
|
| 96 |
+
+ "</div>"
|
| 97 |
+
)
|
| 98 |
+
|
| 99 |
+
return metrics, table_df
|
| 100 |
+
|
| 101 |
+
except Exception:
|
| 102 |
+
err = traceback.format_exc()
|
| 103 |
+
err_html = (
|
| 104 |
+
f'<div style="color:#c94a4a;font-family:JetBrains Mono,monospace;'
|
| 105 |
+
f'font-size:12px;white-space:pre-wrap;">'
|
| 106 |
+
f"Optimizer failed:\n{err}</div>"
|
| 107 |
+
)
|
| 108 |
+
return err_html, pd.DataFrame()
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
# -- Builder -----------------------------------------------------------------
|
| 112 |
+
|
| 113 |
+
def build_optimizer_tab():
|
| 114 |
+
"""Create all Gradio components for the Optimizer tab and wire events.
|
| 115 |
+
|
| 116 |
+
Must be called inside an active ``gr.TabItem(...)`` context manager.
|
| 117 |
+
"""
|
| 118 |
+
|
| 119 |
+
with gr.Row():
|
| 120 |
+
# -- Left panel -- controls -------------------------------------------
|
| 121 |
+
with gr.Column(scale=1, min_width=280):
|
| 122 |
+
gr.HTML(engine_banner("ENGINE: EULER (optimizer default)"))
|
| 123 |
+
|
| 124 |
+
hw_dd = gr.Dropdown(
|
| 125 |
+
choices=["old_icv", "new_icv"],
|
| 126 |
+
value="old_icv",
|
| 127 |
+
label="Hardware Config",
|
| 128 |
+
)
|
| 129 |
+
tgt_dd = gr.Dropdown(
|
| 130 |
+
choices=["max_mdot", "max_mass_eff", "min_kWh", "min_Tc_peak"],
|
| 131 |
+
value="max_mdot",
|
| 132 |
+
label="Objective",
|
| 133 |
+
)
|
| 134 |
+
pexit_sl = gr.Slider(
|
| 135 |
+
minimum=50, maximum=900, value=500, step=10,
|
| 136 |
+
label="Exit Pressure [barg]",
|
| 137 |
+
)
|
| 138 |
+
speed_sl = gr.Slider(
|
| 139 |
+
minimum=0.1, maximum=1.0, value=0.65, step=0.05,
|
| 140 |
+
label="Speed Fraction",
|
| 141 |
+
)
|
| 142 |
+
maxiter_sl = gr.Slider(
|
| 143 |
+
minimum=3, maximum=50, value=10, step=1,
|
| 144 |
+
label="Max Iterations",
|
| 145 |
+
)
|
| 146 |
+
|
| 147 |
+
gr.Markdown("**Constraint (optional):**")
|
| 148 |
+
|
| 149 |
+
c_metric_dd = gr.Dropdown(
|
| 150 |
+
choices=["", "Tc_peak_K", "mass_eff", "kWh_extend", "pc_peak_barg"],
|
| 151 |
+
value="",
|
| 152 |
+
label="Metric",
|
| 153 |
+
)
|
| 154 |
+
c_op_dd = gr.Dropdown(
|
| 155 |
+
choices=["<", ">", "<=", ">="],
|
| 156 |
+
value="<",
|
| 157 |
+
label="Operator",
|
| 158 |
+
)
|
| 159 |
+
c_val_num = gr.Number(value=200, label="Threshold")
|
| 160 |
+
|
| 161 |
+
run_btn = gr.Button("OPTIMIZE", variant="primary")
|
| 162 |
+
|
| 163 |
+
# -- Right panel -- results -------------------------------------------
|
| 164 |
+
with gr.Column(scale=3):
|
| 165 |
+
metrics_out = gr.HTML(label="Metrics")
|
| 166 |
+
table_out = gr.Dataframe(label="Parameter Changes")
|
| 167 |
+
|
| 168 |
+
# -- Wire click event -----------------------------------------------------
|
| 169 |
+
run_btn.click(
|
| 170 |
+
fn=run_optimize,
|
| 171 |
+
inputs=[
|
| 172 |
+
hw_dd, tgt_dd, pexit_sl, speed_sl, maxiter_sl,
|
| 173 |
+
c_metric_dd, c_op_dd, c_val_num,
|
| 174 |
+
],
|
| 175 |
+
outputs=[metrics_out, table_out],
|
| 176 |
+
)
|
murphy_unified/tabs/process_sim.py
CHANGED
|
@@ -28,6 +28,7 @@ from murphy_unified.process_sim.pump_lut import (
|
|
| 28 |
_load_csv,
|
| 29 |
_find_col,
|
| 30 |
DATA_DIR,
|
|
|
|
| 31 |
)
|
| 32 |
from murphy_unified.theme import (
|
| 33 |
ACCENT,
|
|
@@ -485,10 +486,75 @@ def _run_generate_lut(engine, spd_min, spd_max, spd_step, prs_min, prs_max,
|
|
| 485 |
return lut, lut.summary()
|
| 486 |
|
| 487 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 488 |
# ββ Tab builder ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 489 |
|
| 490 |
def build_process_sim_tab():
|
| 491 |
-
"""Build the Process Sim tab. Must be called inside a gr.TabItem context.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 492 |
|
| 493 |
pump_lut_state = gr.State(None)
|
| 494 |
results_state = gr.State(None)
|
|
@@ -500,7 +566,7 @@ def build_process_sim_tab():
|
|
| 500 |
with gr.Column(scale=1):
|
| 501 |
with gr.Accordion("LUT Source", open=True):
|
| 502 |
lut_mode = gr.Radio(
|
| 503 |
-
choices=["Upload CSV", "Auto-Generate from Engine"],
|
| 504 |
value="Upload CSV",
|
| 505 |
label="Pump LUT Source",
|
| 506 |
)
|
|
@@ -536,6 +602,15 @@ def build_process_sim_tab():
|
|
| 536 |
prs_max = gr.Number(label="Pressure max (bar)", value=900)
|
| 537 |
prs_step = gr.Number(label="Pressure step (bar)", value=50)
|
| 538 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 539 |
sim_mode_radio = gr.Radio(
|
| 540 |
choices=SIM_MODES,
|
| 541 |
value=SIM_MODE_VALVE,
|
|
@@ -613,12 +688,13 @@ def build_process_sim_tab():
|
|
| 613 |
return (
|
| 614 |
gr.update(visible=(mode == "Upload CSV")),
|
| 615 |
gr.update(visible=(mode == "Auto-Generate from Engine")),
|
|
|
|
| 616 |
)
|
| 617 |
|
| 618 |
lut_mode.change(
|
| 619 |
fn=_toggle_lut_mode,
|
| 620 |
inputs=[lut_mode],
|
| 621 |
-
outputs=[csv_group, gen_group],
|
| 622 |
api_name=False,
|
| 623 |
)
|
| 624 |
|
|
@@ -686,3 +762,11 @@ def build_process_sim_tab():
|
|
| 686 |
auto_btn.click(fn=_auto_analyze, inputs=auto_inputs, outputs=[chatbot, chat_input], api_name=False)
|
| 687 |
|
| 688 |
clear_chat_btn.click(fn=lambda: ([], ""), inputs=[], outputs=[chatbot, chat_input], api_name=False)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
_load_csv,
|
| 29 |
_find_col,
|
| 30 |
DATA_DIR,
|
| 31 |
+
GENERATED_DIR,
|
| 32 |
)
|
| 33 |
from murphy_unified.theme import (
|
| 34 |
ACCENT,
|
|
|
|
| 486 |
return lut, lut.summary()
|
| 487 |
|
| 488 |
|
| 489 |
+
# ββ Sweep payload ingestion βββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 490 |
+
|
| 491 |
+
def _ingest_sweep_payload(payload):
|
| 492 |
+
"""Build a PumpLUT from a sweep bridge payload; save CSV; return UI updates.
|
| 493 |
+
|
| 494 |
+
Returns
|
| 495 |
+
-------
|
| 496 |
+
(PumpLUT, status_str, lut_mode_update, provenance_str)
|
| 497 |
+
"""
|
| 498 |
+
if payload is None:
|
| 499 |
+
raise gr.Error(
|
| 500 |
+
"No compatible sweep data β run a 2D sweep on "
|
| 501 |
+
"Pexit_barg vs speed_f first."
|
| 502 |
+
)
|
| 503 |
+
|
| 504 |
+
lut = PumpLUT.from_sweep_grid(
|
| 505 |
+
x_vals=payload["x_vals"],
|
| 506 |
+
y_vals=payload["y_vals"],
|
| 507 |
+
z_mdot_kgpm=payload["z_mdot_kgpm"],
|
| 508 |
+
z_temp_K=payload["z_temp_K"],
|
| 509 |
+
axis_x=payload["axis_x"],
|
| 510 |
+
axis_y=payload["axis_y"],
|
| 511 |
+
)
|
| 512 |
+
|
| 513 |
+
# Save CSV to data/generated/ with a unique timestamp.
|
| 514 |
+
import datetime as _dt
|
| 515 |
+
now = _dt.datetime.now()
|
| 516 |
+
ts = now.strftime("%Y-%m-%d_%H%M%S")
|
| 517 |
+
hw = payload.get("hw", "unknown")
|
| 518 |
+
GENERATED_DIR.mkdir(parents=True, exist_ok=True)
|
| 519 |
+
csv_path = GENERATED_DIR / f"lut_{ts}_sweep_{hw}.csv"
|
| 520 |
+
lut.save(str(csv_path))
|
| 521 |
+
|
| 522 |
+
# Status + provenance strings.
|
| 523 |
+
n_speeds = len(lut.speeds)
|
| 524 |
+
n_press = len(lut.pressures)
|
| 525 |
+
status = (
|
| 526 |
+
f"Loaded from sweep ({n_speeds}x{n_press}, hw={hw}, "
|
| 527 |
+
f"{payload.get('timestamp', '')})\n"
|
| 528 |
+
f"{lut.summary()}\n"
|
| 529 |
+
f"Saved: {csv_path.name}"
|
| 530 |
+
)
|
| 531 |
+
provenance = (
|
| 532 |
+
f"Source: 2D sweep (axes: Pexit_barg x speed_f)\n"
|
| 533 |
+
f"Grid: {n_speeds} speeds x {n_press} pressures\n"
|
| 534 |
+
f"Hardware: {hw}\n"
|
| 535 |
+
f"Timestamp: {payload.get('timestamp', '')}\n"
|
| 536 |
+
f"Saved to: {csv_path.name}"
|
| 537 |
+
)
|
| 538 |
+
mode_update = gr.update(value="From Sweep")
|
| 539 |
+
|
| 540 |
+
return lut, status, mode_update, provenance
|
| 541 |
+
|
| 542 |
+
|
| 543 |
# ββ Tab builder ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 544 |
|
| 545 |
def build_process_sim_tab():
|
| 546 |
+
"""Build the Process Sim tab. Must be called inside a gr.TabItem context.
|
| 547 |
+
|
| 548 |
+
Returns
|
| 549 |
+
-------
|
| 550 |
+
dict
|
| 551 |
+
Handles used by app.py to wire cross-tab events:
|
| 552 |
+
* ``pump_lut_state``
|
| 553 |
+
* ``pump_lut_status``
|
| 554 |
+
* ``lut_mode``
|
| 555 |
+
* ``sweep_provenance``
|
| 556 |
+
* ``ingest_fn`` (callable: bridge payload β (lut, status, mode_update, provenance))
|
| 557 |
+
"""
|
| 558 |
|
| 559 |
pump_lut_state = gr.State(None)
|
| 560 |
results_state = gr.State(None)
|
|
|
|
| 566 |
with gr.Column(scale=1):
|
| 567 |
with gr.Accordion("LUT Source", open=True):
|
| 568 |
lut_mode = gr.Radio(
|
| 569 |
+
choices=["Upload CSV", "Auto-Generate from Engine", "From Sweep"],
|
| 570 |
value="Upload CSV",
|
| 571 |
label="Pump LUT Source",
|
| 572 |
)
|
|
|
|
| 602 |
prs_max = gr.Number(label="Pressure max (bar)", value=900)
|
| 603 |
prs_step = gr.Number(label="Pressure step (bar)", value=50)
|
| 604 |
|
| 605 |
+
# From-sweep mode (read-only provenance panel)
|
| 606 |
+
with gr.Group(visible=False) as sweep_group:
|
| 607 |
+
sweep_provenance = gr.Textbox(
|
| 608 |
+
label="Sweep provenance",
|
| 609 |
+
value="No sweep data ingested yet. Run a 2D sweep on "
|
| 610 |
+
"(Pexit_barg, speed_f) and click 'Send to Process Sim'.",
|
| 611 |
+
interactive=False, lines=5,
|
| 612 |
+
)
|
| 613 |
+
|
| 614 |
sim_mode_radio = gr.Radio(
|
| 615 |
choices=SIM_MODES,
|
| 616 |
value=SIM_MODE_VALVE,
|
|
|
|
| 688 |
return (
|
| 689 |
gr.update(visible=(mode == "Upload CSV")),
|
| 690 |
gr.update(visible=(mode == "Auto-Generate from Engine")),
|
| 691 |
+
gr.update(visible=(mode == "From Sweep")),
|
| 692 |
)
|
| 693 |
|
| 694 |
lut_mode.change(
|
| 695 |
fn=_toggle_lut_mode,
|
| 696 |
inputs=[lut_mode],
|
| 697 |
+
outputs=[csv_group, gen_group, sweep_group],
|
| 698 |
api_name=False,
|
| 699 |
)
|
| 700 |
|
|
|
|
| 762 |
auto_btn.click(fn=_auto_analyze, inputs=auto_inputs, outputs=[chatbot, chat_input], api_name=False)
|
| 763 |
|
| 764 |
clear_chat_btn.click(fn=lambda: ([], ""), inputs=[], outputs=[chatbot, chat_input], api_name=False)
|
| 765 |
+
|
| 766 |
+
return {
|
| 767 |
+
"pump_lut_state": pump_lut_state,
|
| 768 |
+
"pump_lut_status": pump_lut_status,
|
| 769 |
+
"lut_mode": lut_mode,
|
| 770 |
+
"sweep_provenance": sweep_provenance,
|
| 771 |
+
"ingest_fn": _ingest_sweep_payload,
|
| 772 |
+
}
|
murphy_unified/tabs/sensitivity.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Tab 4 -- Parameter sensitivity tornado chart via cryosim.
|
| 2 |
+
|
| 3 |
+
Builds all Gradio components (left control panel + right results panel)
|
| 4 |
+
and wires up the click handler. Must be called inside an active
|
| 5 |
+
``gr.TabItem`` context.
|
| 6 |
+
"""
|
| 7 |
+
|
| 8 |
+
from __future__ import annotations
|
| 9 |
+
|
| 10 |
+
import traceback
|
| 11 |
+
|
| 12 |
+
import gradio as gr
|
| 13 |
+
import plotly.graph_objects as go
|
| 14 |
+
|
| 15 |
+
import cryosim
|
| 16 |
+
from murphy_unified.theme import metric_html, TRACE_COLORS, ACCENT, TEXT_DIM, FONT_MONO, engine_banner
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
# -- Click handler -----------------------------------------------------------
|
| 20 |
+
|
| 21 |
+
def run_sensitivity(hardware: str, Pexit: float, speed: float):
|
| 22 |
+
"""Run sensitivity analysis and return (metrics_html, tornado_fig)."""
|
| 23 |
+
|
| 24 |
+
try:
|
| 25 |
+
s = cryosim.sensitivity(hardware=hardware, speed=speed, Pexit=Pexit)
|
| 26 |
+
|
| 27 |
+
ranked = s.rankings["mdot_kgpm"]
|
| 28 |
+
params = [p for p, _ in ranked]
|
| 29 |
+
vals = [v for _, v in ranked]
|
| 30 |
+
|
| 31 |
+
colors = [TRACE_COLORS[1] if v >= 0 else TRACE_COLORS[3] for v in vals]
|
| 32 |
+
|
| 33 |
+
fig = go.Figure(go.Bar(
|
| 34 |
+
y=params,
|
| 35 |
+
x=vals,
|
| 36 |
+
orientation="h",
|
| 37 |
+
marker_color=colors,
|
| 38 |
+
text=[f"{v:+.3f}" for v in vals],
|
| 39 |
+
textposition="outside",
|
| 40 |
+
textfont=dict(family=FONT_MONO, size=11),
|
| 41 |
+
))
|
| 42 |
+
fig.update_layout(
|
| 43 |
+
title=f"Sensitivity: mdot @ {Pexit:.0f} barg (% metric / % param)",
|
| 44 |
+
xaxis_title="Normalized Sensitivity",
|
| 45 |
+
height=350,
|
| 46 |
+
yaxis=dict(tickfont=dict(family=FONT_MONO, size=11)),
|
| 47 |
+
margin=dict(l=120),
|
| 48 |
+
)
|
| 49 |
+
|
| 50 |
+
top = ranked[0]
|
| 51 |
+
metrics = (
|
| 52 |
+
'<div class="metric-row">'
|
| 53 |
+
+ metric_html("TOP PARAMETER", top[0], "", "amber")
|
| 54 |
+
+ metric_html("SENSITIVITY", f"{top[1]:+.3f}", "norm")
|
| 55 |
+
+ metric_html("METRIC", "mdot_kgpm")
|
| 56 |
+
+ "</div>"
|
| 57 |
+
)
|
| 58 |
+
|
| 59 |
+
return metrics, fig
|
| 60 |
+
|
| 61 |
+
except Exception:
|
| 62 |
+
err = traceback.format_exc()
|
| 63 |
+
err_html = (
|
| 64 |
+
f'<div style="color:#c94a4a;font-family:JetBrains Mono,monospace;'
|
| 65 |
+
f'font-size:12px;white-space:pre-wrap;">'
|
| 66 |
+
f"Sensitivity analysis failed:\n{err}</div>"
|
| 67 |
+
)
|
| 68 |
+
empty_fig = go.Figure()
|
| 69 |
+
empty_fig.update_layout(height=350)
|
| 70 |
+
return err_html, empty_fig
|
| 71 |
+
|
| 72 |
+
|
| 73 |
+
# -- Builder -----------------------------------------------------------------
|
| 74 |
+
|
| 75 |
+
def build_sensitivity_tab():
|
| 76 |
+
"""Create all Gradio components for the Sensitivity tab and wire events.
|
| 77 |
+
|
| 78 |
+
Must be called inside an active ``gr.TabItem(...)`` context manager.
|
| 79 |
+
"""
|
| 80 |
+
|
| 81 |
+
with gr.Row():
|
| 82 |
+
# -- Left panel -- controls -------------------------------------------
|
| 83 |
+
with gr.Column(scale=1, min_width=280):
|
| 84 |
+
gr.HTML(engine_banner("ENGINE: EULER"))
|
| 85 |
+
|
| 86 |
+
hw_dd = gr.Dropdown(
|
| 87 |
+
choices=["old_icv", "new_icv"],
|
| 88 |
+
value="old_icv",
|
| 89 |
+
label="Hardware Config",
|
| 90 |
+
)
|
| 91 |
+
pexit_sl = gr.Slider(
|
| 92 |
+
minimum=50, maximum=900, value=500, step=10,
|
| 93 |
+
label="Exit Pressure [barg]",
|
| 94 |
+
)
|
| 95 |
+
speed_sl = gr.Slider(
|
| 96 |
+
minimum=0.1, maximum=1.0, value=0.65, step=0.05,
|
| 97 |
+
label="Speed Fraction",
|
| 98 |
+
)
|
| 99 |
+
|
| 100 |
+
run_btn = gr.Button("ANALYZE", variant="primary")
|
| 101 |
+
|
| 102 |
+
# -- Right panel -- results -------------------------------------------
|
| 103 |
+
with gr.Column(scale=3):
|
| 104 |
+
metrics_out = gr.HTML(label="Metrics")
|
| 105 |
+
plot_out = gr.Plot(label="Tornado Chart")
|
| 106 |
+
|
| 107 |
+
# -- Wire click event -----------------------------------------------------
|
| 108 |
+
run_btn.click(
|
| 109 |
+
fn=run_sensitivity,
|
| 110 |
+
inputs=[hw_dd, pexit_sl, speed_sl],
|
| 111 |
+
outputs=[metrics_out, plot_out],
|
| 112 |
+
)
|
murphy_unified/tabs/simulation.py
CHANGED
|
@@ -15,7 +15,10 @@ Parameter surface mirrors the original DR MURPHY dashboard:
|
|
| 15 |
|
| 16 |
from __future__ import annotations
|
| 17 |
|
|
|
|
|
|
|
| 18 |
import traceback
|
|
|
|
| 19 |
|
| 20 |
import gradio as gr
|
| 21 |
import numpy as np
|
|
@@ -23,6 +26,9 @@ import plotly.graph_objects as go
|
|
| 23 |
|
| 24 |
from murphy_unified.engine_bridge import cycle_sim, ENGINES
|
| 25 |
from murphy_unified.diagnostics import evaluate
|
|
|
|
|
|
|
|
|
|
| 26 |
from murphy_unified.theme import (
|
| 27 |
metric_html, TRACE_COLORS, TEXT_DIM, TEXT_BRIGHT, ACCENT, ACCENT2, WARN, DANGER,
|
| 28 |
FONT_MONO, VALVE_ICV, VALVE_DCV, ENERGY_POS, ENERGY_NEG, TEXT,
|
|
@@ -38,25 +44,6 @@ _ENGINE_MAP = {
|
|
| 38 |
_FLUID_MAP = {"H2": "h2", "N2": "n2"}
|
| 39 |
|
| 40 |
|
| 41 |
-
# ββ MVP 1.0 Simplex defaults (ported from scenario_gui.py) ββββββββββββββββββ
|
| 42 |
-
# ICV: [port, mass, travel, dp_area, Fs, SC, leakKv, comp_eff, npts]
|
| 43 |
-
D_ICV = dict(port=20.5, mass=48.0, travel=5.0, dp_area=779.3,
|
| 44 |
-
Fs=33.4, SC=3.75, leakKv=0.0000736, comp_eff=1.0, npts=100)
|
| 45 |
-
|
| 46 |
-
# DCV: same order
|
| 47 |
-
D_DCV = dict(port=8.3, mass=25.0, travel=3.79, dp_area=78.54,
|
| 48 |
-
Fs=7.8, SC=1.053, leakKv=0.0000026, comp_eff=0.8, npts=200)
|
| 49 |
-
|
| 50 |
-
# Pump: [bore, stroke, hOD, cLen, emH, emS, kvoid, kH, Vfv, vac, cpm, dvf]
|
| 51 |
-
D_PUMP = dict(bore=40.3, stroke=60.0, hOD=120.0, cLen=300.0,
|
| 52 |
-
emH=0.8, emS=0.8, kvoid=0.026, kH=15.0,
|
| 53 |
-
Vfv=37.0, vac=760000.0, cpm=500.0, dvf=0.01)
|
| 54 |
-
|
| 55 |
-
# Proc: [Tamb, htc_amb, net_drive, F_mult, Kv_BB, Pbb_exit, fric2chamber, Exp_eff]
|
| 56 |
-
D_PROC = dict(Tamb=300.0, htc=10.0, drive=4.0, Fmult=30.0,
|
| 57 |
-
KvBB=0.006, Pbb=0.0, fric=0.5, Exp=0.2)
|
| 58 |
-
|
| 59 |
-
|
| 60 |
# ββ Extra plot definitions ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 61 |
# Each entry: (hist_key, y-axis label, trace color index)
|
| 62 |
# "Energy Balance" is a special case (bar chart, not time-series).
|
|
@@ -230,32 +217,33 @@ def run_simulation(
|
|
| 230 |
):
|
| 231 |
"""Run one pump cycle with the full parameter set and return plots + diagnostics."""
|
| 232 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 233 |
engine_id = _ENGINE_MAP.get(engine_name, "euler")
|
| 234 |
fluid_id = _FLUID_MAP.get(fluid, "h2")
|
| 235 |
|
| 236 |
-
# ββ Build parameter arrays βββββββββββββββββββββββββββββββββββββββββββ
|
| 237 |
-
ICVparam = [icv_port, icv_mass, icv_travel, icv_dp_area,
|
| 238 |
-
icv_Fs, icv_SC, icv_leakKv, icv_comp_eff, int(icv_npts)]
|
| 239 |
-
DCVparam = [dcv_port, dcv_mass, dcv_travel, dcv_dp_area,
|
| 240 |
-
dcv_Fs, dcv_SC, dcv_leakKv, dcv_comp_eff, int(dcv_npts)]
|
| 241 |
-
pump_geom = [bore, stroke, hOD, cLen, emH, emS, kvoid, kH,
|
| 242 |
-
Vfv, vac, cpm, dvf]
|
| 243 |
-
proc_param = [Tamb, htc, drive, Fmult, KvBB, Pbb, fric, Exp]
|
| 244 |
-
|
| 245 |
-
# ββ Build exit_param if downstream mode is selected ββββββββββββββββββ
|
| 246 |
-
# [mode, snubber_L, chss_L, aov140_frac, ro_dia_mm] β mode: 1=CHSS, 0=RO
|
| 247 |
-
exit_param = None
|
| 248 |
-
if fill_type == "CHSS Fill":
|
| 249 |
-
exit_param = [1, vsnubber, vchss, aov140f, rodia]
|
| 250 |
-
elif fill_type == "RO Vent":
|
| 251 |
-
exit_param = [0, vsnubber, 0, aov140f, rodia]
|
| 252 |
-
|
| 253 |
-
extra_kwargs: dict = {}
|
| 254 |
-
if exit_param is not None:
|
| 255 |
-
extra_kwargs["exit_param"] = exit_param
|
| 256 |
n_cyc = int(num_cycles) if num_cycles else 1
|
| 257 |
-
if n_cyc > 1:
|
| 258 |
-
extra_kwargs["n_cycles"] = n_cyc
|
| 259 |
|
| 260 |
# ββ Parameter summary (shown above plots on every run) ββββββββββββββ
|
| 261 |
params_html = _build_params_summary_html(
|
|
@@ -267,22 +255,10 @@ def run_simulation(
|
|
| 267 |
fill_type, vsnubber, vchss, aov140f, rodia, n_cyc,
|
| 268 |
)
|
| 269 |
|
| 270 |
-
# ββ Call engine bridge βββββββββββββββββββββββββββββ
|
|
|
|
| 271 |
try:
|
| 272 |
-
out, hist = cycle_sim(
|
| 273 |
-
engine=engine_id,
|
| 274 |
-
fluid=fluid_id,
|
| 275 |
-
Pexit_barg=Pexit,
|
| 276 |
-
speed_f=speed,
|
| 277 |
-
Ptank_barg=Ptank,
|
| 278 |
-
Psat_barg=Psat,
|
| 279 |
-
ICVparam=ICVparam,
|
| 280 |
-
DCVparam=DCVparam,
|
| 281 |
-
pump_geom=pump_geom,
|
| 282 |
-
proc_param=proc_param,
|
| 283 |
-
flash_eff=flash_eff,
|
| 284 |
-
**extra_kwargs,
|
| 285 |
-
)
|
| 286 |
except Exception:
|
| 287 |
err = traceback.format_exc()
|
| 288 |
err_html = (
|
|
@@ -291,7 +267,8 @@ def run_simulation(
|
|
| 291 |
f'Simulation failed:\n{err}</div>'
|
| 292 |
)
|
| 293 |
ef = _empty_fig()
|
| 294 |
-
|
|
|
|
| 295 |
|
| 296 |
# ββ Extract scalars (prefer hist dict) βββββββββββββββββββββββββββββββ
|
| 297 |
mdot = hist.get("mdot_kgpm", 0.0)
|
|
@@ -455,12 +432,62 @@ def run_simulation(
|
|
| 455 |
status_html = f'<div class="status-bar">{" | ".join(parts)}</div>'
|
| 456 |
|
| 457 |
return (metrics_html, params_html, fig_pt, fig_valves,
|
| 458 |
-
extra_figs[0], extra_figs[1], status_html)
|
| 459 |
|
| 460 |
|
| 461 |
# ββ Builder ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 462 |
|
| 463 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 464 |
"""Create all Gradio components for the Simulation tab and wire events.
|
| 465 |
|
| 466 |
Must be called inside an active ``gr.TabItem(...)`` context manager.
|
|
@@ -473,119 +500,129 @@ def build_simulation_tab():
|
|
| 473 |
# ββ Operating conditions (always visible) βββββββββββββββββββ
|
| 474 |
engine_dd = gr.Dropdown(
|
| 475 |
choices=["Euler", "Lightspeed", "General-ODE"],
|
| 476 |
-
value="
|
| 477 |
label="Engine",
|
| 478 |
)
|
| 479 |
fluid_dd = gr.Dropdown(
|
| 480 |
choices=["H2", "N2"],
|
| 481 |
-
value="
|
| 482 |
label="Fluid",
|
| 483 |
)
|
| 484 |
pexit_sl = gr.Slider(
|
| 485 |
-
minimum=50, maximum=1100, value=
|
| 486 |
label="Exit Pressure [barg]",
|
| 487 |
)
|
| 488 |
speed_sl = gr.Slider(
|
| 489 |
-
minimum=0.1, maximum=1.0, value=
|
| 490 |
label="Speed Fraction",
|
| 491 |
)
|
| 492 |
with gr.Row():
|
| 493 |
-
ptank_num = gr.Number(value=
|
| 494 |
-
psat_num = gr.Number(value=
|
| 495 |
|
| 496 |
run_btn = gr.Button("RUN CYCLE", variant="primary")
|
| 497 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 498 |
# ββ ICV Parameters ββββββββββββββββββββββββββββββββββββββββββ
|
| 499 |
with gr.Accordion("ICV Parameters", open=False):
|
| 500 |
with gr.Row():
|
| 501 |
-
icv_port = gr.Number(value=
|
| 502 |
-
icv_mass = gr.Number(value=
|
| 503 |
-
icv_travel = gr.Number(value=
|
| 504 |
with gr.Row():
|
| 505 |
-
icv_dparea = gr.Number(value=
|
| 506 |
-
icv_Fs = gr.Number(value=
|
| 507 |
-
icv_SC = gr.Number(value=
|
| 508 |
with gr.Row():
|
| 509 |
-
icv_leak = gr.Number(value=
|
| 510 |
-
icv_ceff = gr.Number(value=
|
| 511 |
-
icv_npts = gr.Number(value=
|
| 512 |
|
| 513 |
# ββ DCV Parameters ββββββββββββββββββββββββββββββββββββββββββ
|
| 514 |
with gr.Accordion("DCV Parameters", open=False):
|
| 515 |
with gr.Row():
|
| 516 |
-
dcv_port = gr.Number(value=
|
| 517 |
-
dcv_mass = gr.Number(value=
|
| 518 |
-
dcv_travel = gr.Number(value=
|
| 519 |
with gr.Row():
|
| 520 |
-
dcv_dparea = gr.Number(value=
|
| 521 |
-
dcv_Fs = gr.Number(value=
|
| 522 |
-
dcv_SC = gr.Number(value=
|
| 523 |
with gr.Row():
|
| 524 |
-
dcv_leak = gr.Number(value=
|
| 525 |
-
dcv_ceff = gr.Number(value=
|
| 526 |
-
dcv_npts = gr.Number(value=
|
| 527 |
|
| 528 |
# ββ Pump Geometry βββββββββββββββββββββββββββββββββββββββββββ
|
| 529 |
with gr.Accordion("Pump Geometry", open=False):
|
| 530 |
with gr.Row():
|
| 531 |
-
bore = gr.Number(value=
|
| 532 |
-
stroke = gr.Number(value=
|
| 533 |
with gr.Row():
|
| 534 |
-
cpm = gr.Number(value=
|
| 535 |
-
dvf = gr.Number(value=
|
| 536 |
with gr.Row():
|
| 537 |
-
hOD = gr.Number(value=
|
| 538 |
-
cLen = gr.Number(value=
|
| 539 |
with gr.Row():
|
| 540 |
-
emH = gr.Number(value=
|
| 541 |
-
emS = gr.Number(value=
|
| 542 |
with gr.Row():
|
| 543 |
-
kvoid = gr.Number(value=
|
| 544 |
-
kH = gr.Number(value=
|
| 545 |
with gr.Row():
|
| 546 |
-
Vfv = gr.Number(value=
|
| 547 |
-
vac = gr.Number(value=
|
| 548 |
|
| 549 |
# ββ Process / Losses ββββββββββββββββββββββββββββββββββββββββ
|
| 550 |
with gr.Accordion("Process / Losses", open=False):
|
| 551 |
with gr.Row():
|
| 552 |
-
Tamb = gr.Number(value=
|
| 553 |
-
htc = gr.Number(value=
|
| 554 |
with gr.Row():
|
| 555 |
-
drive = gr.Number(value=
|
| 556 |
-
Fmult = gr.Number(value=
|
| 557 |
with gr.Row():
|
| 558 |
-
fric = gr.Number(value=
|
| 559 |
-
KvBB = gr.Number(value=
|
| 560 |
with gr.Row():
|
| 561 |
-
Pbb = gr.Number(value=
|
| 562 |
-
Exp = gr.Number(value=
|
| 563 |
-
flash_eff = gr.Number(value=
|
| 564 |
|
| 565 |
# ββ Downstream / Fill mode ββββββββββββββββββββββββββββββββββ
|
| 566 |
with gr.Accordion("Downstream / Fill Mode", open=False):
|
| 567 |
with gr.Row():
|
| 568 |
fill_type = gr.Dropdown(
|
| 569 |
choices=["Fixed Pexit", "CHSS Fill", "RO Vent"],
|
| 570 |
-
value="
|
| 571 |
)
|
| 572 |
num_cycles = gr.Number(
|
| 573 |
-
value=
|
| 574 |
step=1, minimum=1, maximum=50, precision=0,
|
| 575 |
)
|
| 576 |
with gr.Row():
|
| 577 |
-
vsnubber = gr.Number(value=
|
| 578 |
-
vchss = gr.Number(value=
|
| 579 |
with gr.Row():
|
| 580 |
-
aov140f = gr.Number(value=
|
| 581 |
minimum=0, maximum=1)
|
| 582 |
-
rodia = gr.Number(value=
|
| 583 |
|
| 584 |
# ββ Extra plots selector ββββββββββββββββββββββββββββββββββββ
|
| 585 |
gr.Markdown("**Additional plots** (select up to 2):")
|
| 586 |
extras_cb = gr.CheckboxGroup(
|
| 587 |
choices=EXTRA_PLOT_CHOICES,
|
| 588 |
-
value=["
|
| 589 |
label="Extra Graphs",
|
| 590 |
)
|
| 591 |
|
|
@@ -623,5 +660,31 @@ def build_simulation_tab():
|
|
| 623 |
extras_cb,
|
| 624 |
],
|
| 625 |
outputs=[metrics_out, params_out, fig_pt_out, fig_valves_out,
|
| 626 |
-
fig_extra1_out, fig_extra2_out, status_out],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 627 |
)
|
|
|
|
| 15 |
|
| 16 |
from __future__ import annotations
|
| 17 |
|
| 18 |
+
import os
|
| 19 |
+
import tempfile
|
| 20 |
import traceback
|
| 21 |
+
from datetime import datetime, timezone
|
| 22 |
|
| 23 |
import gradio as gr
|
| 24 |
import numpy as np
|
|
|
|
| 26 |
|
| 27 |
from murphy_unified.engine_bridge import cycle_sim, ENGINES
|
| 28 |
from murphy_unified.diagnostics import evaluate
|
| 29 |
+
from murphy_unified.engine_params import build_engine_kwargs
|
| 30 |
+
from murphy_unified.params_io import params_to_csv, csv_to_params
|
| 31 |
+
from murphy_unified.sim_defaults import SIM_DEFAULTS
|
| 32 |
from murphy_unified.theme import (
|
| 33 |
metric_html, TRACE_COLORS, TEXT_DIM, TEXT_BRIGHT, ACCENT, ACCENT2, WARN, DANGER,
|
| 34 |
FONT_MONO, VALVE_ICV, VALVE_DCV, ENERGY_POS, ENERGY_NEG, TEXT,
|
|
|
|
| 44 |
_FLUID_MAP = {"H2": "h2", "N2": "n2"}
|
| 45 |
|
| 46 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
# ββ Extra plot definitions ββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 48 |
# Each entry: (hist_key, y-axis label, trace color index)
|
| 49 |
# "Energy Balance" is a special case (bar chart, not time-series).
|
|
|
|
| 217 |
):
|
| 218 |
"""Run one pump cycle with the full parameter set and return plots + diagnostics."""
|
| 219 |
|
| 220 |
+
# ββ Snapshot submitted params BEFORE the engine call so sim_params_state
|
| 221 |
+
# reflects user inputs even if cycle_sim raises.
|
| 222 |
+
sim_state: dict = {
|
| 223 |
+
"engine_name": engine_name, "fluid": fluid,
|
| 224 |
+
"Pexit": Pexit, "speed": speed, "Ptank": Ptank, "Psat": Psat,
|
| 225 |
+
"icv_port": icv_port, "icv_mass": icv_mass, "icv_travel": icv_travel,
|
| 226 |
+
"icv_dp_area": icv_dp_area, "icv_Fs": icv_Fs, "icv_SC": icv_SC,
|
| 227 |
+
"icv_leakKv": icv_leakKv, "icv_comp_eff": icv_comp_eff, "icv_npts": icv_npts,
|
| 228 |
+
"dcv_port": dcv_port, "dcv_mass": dcv_mass, "dcv_travel": dcv_travel,
|
| 229 |
+
"dcv_dp_area": dcv_dp_area, "dcv_Fs": dcv_Fs, "dcv_SC": dcv_SC,
|
| 230 |
+
"dcv_leakKv": dcv_leakKv, "dcv_comp_eff": dcv_comp_eff, "dcv_npts": dcv_npts,
|
| 231 |
+
"bore": bore, "stroke": stroke, "hOD": hOD, "cLen": cLen,
|
| 232 |
+
"emH": emH, "emS": emS, "kvoid": kvoid, "kH": kH,
|
| 233 |
+
"Vfv": Vfv, "vac": vac, "cpm": cpm, "dvf": dvf,
|
| 234 |
+
"Tamb": Tamb, "htc": htc, "drive": drive, "Fmult": Fmult,
|
| 235 |
+
"KvBB": KvBB, "Pbb": Pbb, "fric": fric, "Exp": Exp, "flash_eff": flash_eff,
|
| 236 |
+
"fill_type": fill_type, "num_cycles": num_cycles,
|
| 237 |
+
"vsnubber": vsnubber, "vchss": vchss, "aov140f": aov140f, "rodia": rodia,
|
| 238 |
+
"extra_plots": list(extra_plots or []),
|
| 239 |
+
"_written_at": datetime.now(timezone.utc).isoformat(timespec="seconds"),
|
| 240 |
+
"_engine_ok": True,
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
engine_id = _ENGINE_MAP.get(engine_name, "euler")
|
| 244 |
fluid_id = _FLUID_MAP.get(fluid, "h2")
|
| 245 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 246 |
n_cyc = int(num_cycles) if num_cycles else 1
|
|
|
|
|
|
|
| 247 |
|
| 248 |
# ββ Parameter summary (shown above plots on every run) ββββββββββββββ
|
| 249 |
params_html = _build_params_summary_html(
|
|
|
|
| 255 |
fill_type, vsnubber, vchss, aov140f, rodia, n_cyc,
|
| 256 |
)
|
| 257 |
|
| 258 |
+
# ββ Call engine bridge via shared helper βββββββββββββββββββββββββββββ
|
| 259 |
+
engine_kwargs = build_engine_kwargs(sim_state)
|
| 260 |
try:
|
| 261 |
+
out, hist = cycle_sim(engine=engine_id, fluid=fluid_id, **engine_kwargs)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 262 |
except Exception:
|
| 263 |
err = traceback.format_exc()
|
| 264 |
err_html = (
|
|
|
|
| 267 |
f'Simulation failed:\n{err}</div>'
|
| 268 |
)
|
| 269 |
ef = _empty_fig()
|
| 270 |
+
sim_state["_engine_ok"] = False
|
| 271 |
+
return err_html, params_html, ef, ef, ef, ef, err_html, sim_state
|
| 272 |
|
| 273 |
# ββ Extract scalars (prefer hist dict) βββββββββββββββββββββββββββββββ
|
| 274 |
mdot = hist.get("mdot_kgpm", 0.0)
|
|
|
|
| 432 |
status_html = f'<div class="status-bar">{" | ".join(parts)}</div>'
|
| 433 |
|
| 434 |
return (metrics_html, params_html, fig_pt, fig_valves,
|
| 435 |
+
extra_figs[0], extra_figs[1], status_html, sim_state)
|
| 436 |
|
| 437 |
|
| 438 |
# ββ Builder ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 439 |
|
| 440 |
+
_WIDGET_KEYS_IN_ORDER = (
|
| 441 |
+
"engine_name", "fluid", "Pexit", "speed", "Ptank", "Psat",
|
| 442 |
+
"icv_port", "icv_mass", "icv_travel", "icv_dp_area",
|
| 443 |
+
"icv_Fs", "icv_SC", "icv_leakKv", "icv_comp_eff", "icv_npts",
|
| 444 |
+
"dcv_port", "dcv_mass", "dcv_travel", "dcv_dp_area",
|
| 445 |
+
"dcv_Fs", "dcv_SC", "dcv_leakKv", "dcv_comp_eff", "dcv_npts",
|
| 446 |
+
"bore", "stroke", "hOD", "cLen", "emH", "emS", "kvoid", "kH",
|
| 447 |
+
"Vfv", "vac", "cpm", "dvf",
|
| 448 |
+
"Tamb", "htc", "drive", "Fmult", "KvBB", "Pbb", "fric", "Exp", "flash_eff",
|
| 449 |
+
"fill_type", "num_cycles", "vsnubber", "vchss", "aov140f", "rodia",
|
| 450 |
+
"extra_plots",
|
| 451 |
+
)
|
| 452 |
+
|
| 453 |
+
|
| 454 |
+
def _export_params(*widget_values) -> str:
|
| 455 |
+
"""Write current widget state to a tempfile CSV; return path for gr.File."""
|
| 456 |
+
state = dict(zip(_WIDGET_KEYS_IN_ORDER, widget_values))
|
| 457 |
+
state["extra_plots"] = list(state.get("extra_plots") or [])
|
| 458 |
+
csv_text = params_to_csv(state)
|
| 459 |
+
stamp = datetime.utcnow().strftime("%Y%m%d-%H%M%S")
|
| 460 |
+
path = os.path.join(tempfile.gettempdir(), f"murphy_run_{stamp}.csv")
|
| 461 |
+
with open(path, "w", encoding="utf-8", newline="") as f:
|
| 462 |
+
f.write(csv_text)
|
| 463 |
+
return path
|
| 464 |
+
|
| 465 |
+
|
| 466 |
+
def _import_params(file_obj):
|
| 467 |
+
"""Load a CSV and return widget updates in _WIDGET_KEYS_IN_ORDER + info HTML."""
|
| 468 |
+
if file_obj is None:
|
| 469 |
+
return (*([gr.update()] * len(_WIDGET_KEYS_IN_ORDER)), "")
|
| 470 |
+
path = file_obj if isinstance(file_obj, str) else file_obj.name
|
| 471 |
+
with open(path, "r", encoding="utf-8") as f:
|
| 472 |
+
csv_text = f.read()
|
| 473 |
+
parsed, skipped = csv_to_params(csv_text)
|
| 474 |
+
|
| 475 |
+
updates = tuple(
|
| 476 |
+
gr.update(value=parsed[key]) if key in parsed else gr.update()
|
| 477 |
+
for key in _WIDGET_KEYS_IN_ORDER
|
| 478 |
+
)
|
| 479 |
+
loaded = len(parsed)
|
| 480 |
+
total = len(SIM_DEFAULTS)
|
| 481 |
+
skipped_msg = f" Skipped: {', '.join(skipped)}" if skipped else ""
|
| 482 |
+
info_html = (
|
| 483 |
+
f'<div style="font-family:JetBrains Mono,monospace;font-size:11px;'
|
| 484 |
+
f'color:{TEXT_DIM};padding:4px 0;">'
|
| 485 |
+
f'Loaded {loaded}/{total} params.{skipped_msg}</div>'
|
| 486 |
+
)
|
| 487 |
+
return (*updates, info_html)
|
| 488 |
+
|
| 489 |
+
|
| 490 |
+
def build_simulation_tab(sim_params_state: gr.State):
|
| 491 |
"""Create all Gradio components for the Simulation tab and wire events.
|
| 492 |
|
| 493 |
Must be called inside an active ``gr.TabItem(...)`` context manager.
|
|
|
|
| 500 |
# ββ Operating conditions (always visible) βββββββββββββββββββ
|
| 501 |
engine_dd = gr.Dropdown(
|
| 502 |
choices=["Euler", "Lightspeed", "General-ODE"],
|
| 503 |
+
value=SIM_DEFAULTS["engine_name"],
|
| 504 |
label="Engine",
|
| 505 |
)
|
| 506 |
fluid_dd = gr.Dropdown(
|
| 507 |
choices=["H2", "N2"],
|
| 508 |
+
value=SIM_DEFAULTS["fluid"],
|
| 509 |
label="Fluid",
|
| 510 |
)
|
| 511 |
pexit_sl = gr.Slider(
|
| 512 |
+
minimum=50, maximum=1100, value=SIM_DEFAULTS["Pexit"], step=10,
|
| 513 |
label="Exit Pressure [barg]",
|
| 514 |
)
|
| 515 |
speed_sl = gr.Slider(
|
| 516 |
+
minimum=0.1, maximum=1.0, value=SIM_DEFAULTS["speed"], step=0.05,
|
| 517 |
label="Speed Fraction",
|
| 518 |
)
|
| 519 |
with gr.Row():
|
| 520 |
+
ptank_num = gr.Number(value=SIM_DEFAULTS["Ptank"], label="Tank P [barg]", step=0.1)
|
| 521 |
+
psat_num = gr.Number(value=SIM_DEFAULTS["Psat"], label="Sat P [barg]", step=0.1)
|
| 522 |
|
| 523 |
run_btn = gr.Button("RUN CYCLE", variant="primary")
|
| 524 |
|
| 525 |
+
# ββ Save / Load run params ββββββββββββββββββββββββββββββββββ
|
| 526 |
+
with gr.Row():
|
| 527 |
+
save_btn = gr.DownloadButton("Save params to CSV")
|
| 528 |
+
load_file = gr.File(
|
| 529 |
+
label="Load params from CSV",
|
| 530 |
+
file_types=[".csv"],
|
| 531 |
+
type="filepath",
|
| 532 |
+
)
|
| 533 |
+
load_info_html = gr.HTML()
|
| 534 |
+
|
| 535 |
# ββ ICV Parameters ββββββββββββββββββββββββββββββββββββββββββ
|
| 536 |
with gr.Accordion("ICV Parameters", open=False):
|
| 537 |
with gr.Row():
|
| 538 |
+
icv_port = gr.Number(value=SIM_DEFAULTS['icv_port'], label="Port Dia (mm)", step=0.1)
|
| 539 |
+
icv_mass = gr.Number(value=SIM_DEFAULTS['icv_mass'], label="Mass (g)", step=0.1)
|
| 540 |
+
icv_travel = gr.Number(value=SIM_DEFAULTS['icv_travel'], label="Travel (mm)", step=0.1)
|
| 541 |
with gr.Row():
|
| 542 |
+
icv_dparea = gr.Number(value=SIM_DEFAULTS['icv_dp_area'], label="dP Area (mmΒ²)", step=0.1)
|
| 543 |
+
icv_Fs = gr.Number(value=SIM_DEFAULTS['icv_Fs'], label="Spring F (N)", step=0.1)
|
| 544 |
+
icv_SC = gr.Number(value=SIM_DEFAULTS['icv_SC'], label="Spring K (N/mm)", step=0.01)
|
| 545 |
with gr.Row():
|
| 546 |
+
icv_leak = gr.Number(value=SIM_DEFAULTS['icv_leakKv'], label="Leak Kv (mΒ³/hr)", step=1e-7)
|
| 547 |
+
icv_ceff = gr.Number(value=SIM_DEFAULTS['icv_comp_eff'], label="Comp Eff", step=0.01)
|
| 548 |
+
icv_npts = gr.Number(value=SIM_DEFAULTS['icv_npts'], label="Npts", step=1, precision=0)
|
| 549 |
|
| 550 |
# ββ DCV Parameters ββββββββββββββββββββββββββββββββββββββββββ
|
| 551 |
with gr.Accordion("DCV Parameters", open=False):
|
| 552 |
with gr.Row():
|
| 553 |
+
dcv_port = gr.Number(value=SIM_DEFAULTS['dcv_port'], label="Port Dia (mm)", step=0.1)
|
| 554 |
+
dcv_mass = gr.Number(value=SIM_DEFAULTS['dcv_mass'], label="Mass (g)", step=0.1)
|
| 555 |
+
dcv_travel = gr.Number(value=SIM_DEFAULTS['dcv_travel'], label="Travel (mm)", step=0.01)
|
| 556 |
with gr.Row():
|
| 557 |
+
dcv_dparea = gr.Number(value=SIM_DEFAULTS['dcv_dp_area'], label="dP Area (mmΒ²)", step=0.01)
|
| 558 |
+
dcv_Fs = gr.Number(value=SIM_DEFAULTS['dcv_Fs'], label="Spring F (N)", step=0.1)
|
| 559 |
+
dcv_SC = gr.Number(value=SIM_DEFAULTS['dcv_SC'], label="Spring K (N/mm)", step=0.001)
|
| 560 |
with gr.Row():
|
| 561 |
+
dcv_leak = gr.Number(value=SIM_DEFAULTS['dcv_leakKv'], label="Leak Kv (mΒ³/hr)", step=1e-7)
|
| 562 |
+
dcv_ceff = gr.Number(value=SIM_DEFAULTS['dcv_comp_eff'], label="Comp Eff", step=0.01)
|
| 563 |
+
dcv_npts = gr.Number(value=SIM_DEFAULTS['dcv_npts'], label="Npts", step=1, precision=0)
|
| 564 |
|
| 565 |
# ββ Pump Geometry βββββββββββββββββββββββββββββββββββββββββββ
|
| 566 |
with gr.Accordion("Pump Geometry", open=False):
|
| 567 |
with gr.Row():
|
| 568 |
+
bore = gr.Number(value=SIM_DEFAULTS['bore'], label="Bore (mm)", step=0.1)
|
| 569 |
+
stroke = gr.Number(value=SIM_DEFAULTS['stroke'], label="Stroke (mm)", step=0.1)
|
| 570 |
with gr.Row():
|
| 571 |
+
cpm = gr.Number(value=SIM_DEFAULTS['cpm'], label="Speed (cpm)", step=1)
|
| 572 |
+
dvf = gr.Number(value=SIM_DEFAULTS['dvf'], label="Dead Vol Frac", step=0.001)
|
| 573 |
with gr.Row():
|
| 574 |
+
hOD = gr.Number(value=SIM_DEFAULTS['hOD'], label="Housing OD (mm)", step=1)
|
| 575 |
+
cLen = gr.Number(value=SIM_DEFAULTS['cLen'], label="Chamber Len (mm)", step=1)
|
| 576 |
with gr.Row():
|
| 577 |
+
emH = gr.Number(value=SIM_DEFAULTS['emH'], label="Ξ΅ Housing", step=0.01)
|
| 578 |
+
emS = gr.Number(value=SIM_DEFAULTS['emS'], label="Ξ΅ Shield", step=0.01)
|
| 579 |
with gr.Row():
|
| 580 |
+
kvoid = gr.Number(value=SIM_DEFAULTS['kvoid'], label="k void (W/m/K)", step=0.001)
|
| 581 |
+
kH = gr.Number(value=SIM_DEFAULTS['kH'], label="k housing (W/m/K)", step=0.1)
|
| 582 |
with gr.Row():
|
| 583 |
+
Vfv = gr.Number(value=SIM_DEFAULTS['Vfv'], label="Void Vol Frac", step=0.1)
|
| 584 |
+
vac = gr.Number(value=SIM_DEFAULTS['vac'], label="Vacuum (Β΅Hg)", step=1000)
|
| 585 |
|
| 586 |
# ββ Process / Losses ββββββββββββββββββββββββββββββββββββββββ
|
| 587 |
with gr.Accordion("Process / Losses", open=False):
|
| 588 |
with gr.Row():
|
| 589 |
+
Tamb = gr.Number(value=SIM_DEFAULTS['Tamb'], label="T_amb (K)", step=1)
|
| 590 |
+
htc = gr.Number(value=SIM_DEFAULTS['htc'], label="HTC (W/mΒ²/K)", step=0.1)
|
| 591 |
with gr.Row():
|
| 592 |
+
drive = gr.Number(value=SIM_DEFAULTS['drive'], label="Drive (kgf)", step=0.1)
|
| 593 |
+
Fmult = gr.Number(value=SIM_DEFAULTS['Fmult'], label="F Multiplier", step=1)
|
| 594 |
with gr.Row():
|
| 595 |
+
fric = gr.Number(value=SIM_DEFAULTS['fric'], label="Friction (0-1)", step=0.01)
|
| 596 |
+
KvBB = gr.Number(value=SIM_DEFAULTS['KvBB'], label="BB Kv (mΒ³/hr)", step=0.001)
|
| 597 |
with gr.Row():
|
| 598 |
+
Pbb = gr.Number(value=SIM_DEFAULTS['Pbb'], label="BB Exit P (barg)", step=0.1)
|
| 599 |
+
Exp = gr.Number(value=SIM_DEFAULTS['Exp'], label="Exp Eff (0-1)", step=0.01)
|
| 600 |
+
flash_eff = gr.Number(value=SIM_DEFAULTS["flash_eff"], label="Thermal Mass Eff (0-1)", step=0.01)
|
| 601 |
|
| 602 |
# ββ Downstream / Fill mode ββββββββββββββββββββββββββββββββββ
|
| 603 |
with gr.Accordion("Downstream / Fill Mode", open=False):
|
| 604 |
with gr.Row():
|
| 605 |
fill_type = gr.Dropdown(
|
| 606 |
choices=["Fixed Pexit", "CHSS Fill", "RO Vent"],
|
| 607 |
+
value=SIM_DEFAULTS["fill_type"], label="Fill Type",
|
| 608 |
)
|
| 609 |
num_cycles = gr.Number(
|
| 610 |
+
value=SIM_DEFAULTS["num_cycles"], label="No. cycles",
|
| 611 |
step=1, minimum=1, maximum=50, precision=0,
|
| 612 |
)
|
| 613 |
with gr.Row():
|
| 614 |
+
vsnubber = gr.Number(value=SIM_DEFAULTS["vsnubber"], label="Snubber (L)", step=0.1)
|
| 615 |
+
vchss = gr.Number(value=SIM_DEFAULTS["vchss"], label="CHSS (L)", step=1)
|
| 616 |
with gr.Row():
|
| 617 |
+
aov140f = gr.Number(value=SIM_DEFAULTS["aov140f"], label="AOV140 (0-1)", step=0.01,
|
| 618 |
minimum=0, maximum=1)
|
| 619 |
+
rodia = gr.Number(value=SIM_DEFAULTS["rodia"], label="RO Dia (mm)", step=0.01)
|
| 620 |
|
| 621 |
# ββ Extra plots selector ββββββββββββββββββββββββββββββββββββ
|
| 622 |
gr.Markdown("**Additional plots** (select up to 2):")
|
| 623 |
extras_cb = gr.CheckboxGroup(
|
| 624 |
choices=EXTRA_PLOT_CHOICES,
|
| 625 |
+
value=SIM_DEFAULTS["extra_plots"],
|
| 626 |
label="Extra Graphs",
|
| 627 |
)
|
| 628 |
|
|
|
|
| 660 |
extras_cb,
|
| 661 |
],
|
| 662 |
outputs=[metrics_out, params_out, fig_pt_out, fig_valves_out,
|
| 663 |
+
fig_extra1_out, fig_extra2_out, status_out, sim_params_state],
|
| 664 |
+
)
|
| 665 |
+
|
| 666 |
+
# ββ Widget list (order must match _WIDGET_KEYS_IN_ORDER) βββββββββββββ
|
| 667 |
+
_all_widgets = [
|
| 668 |
+
engine_dd, fluid_dd, pexit_sl, speed_sl, ptank_num, psat_num,
|
| 669 |
+
icv_port, icv_mass, icv_travel, icv_dparea,
|
| 670 |
+
icv_Fs, icv_SC, icv_leak, icv_ceff, icv_npts,
|
| 671 |
+
dcv_port, dcv_mass, dcv_travel, dcv_dparea,
|
| 672 |
+
dcv_Fs, dcv_SC, dcv_leak, dcv_ceff, dcv_npts,
|
| 673 |
+
bore, stroke, hOD, cLen, emH, emS, kvoid, kH,
|
| 674 |
+
Vfv, vac, cpm, dvf,
|
| 675 |
+
Tamb, htc, drive, Fmult, KvBB, Pbb, fric, Exp, flash_eff,
|
| 676 |
+
fill_type, num_cycles, vsnubber, vchss, aov140f, rodia,
|
| 677 |
+
extras_cb,
|
| 678 |
+
]
|
| 679 |
+
|
| 680 |
+
save_btn.click(
|
| 681 |
+
fn=_export_params,
|
| 682 |
+
inputs=_all_widgets,
|
| 683 |
+
outputs=[save_btn],
|
| 684 |
+
)
|
| 685 |
+
|
| 686 |
+
load_file.change(
|
| 687 |
+
fn=_import_params,
|
| 688 |
+
inputs=[load_file],
|
| 689 |
+
outputs=[*_all_widgets, load_info_html],
|
| 690 |
)
|
murphy_unified/tabs/sweep.py
CHANGED
|
@@ -17,6 +17,8 @@ import pandas as pd
|
|
| 17 |
import plotly.graph_objects as go
|
| 18 |
|
| 19 |
from murphy_unified.engine_bridge import cycle_sim
|
|
|
|
|
|
|
| 20 |
from murphy_unified.sweep_cache import SweepCache
|
| 21 |
from murphy_unified.theme import (
|
| 22 |
metric_html,
|
|
@@ -56,8 +58,10 @@ SWEEP_PARAMS = {
|
|
| 56 |
"htc": {"label": "Heat Transfer Coeff", "min": 0.0, "max": 100.0, "default": 10.0},
|
| 57 |
}
|
| 58 |
|
| 59 |
-
# ββ
|
| 60 |
-
#
|
|
|
|
|
|
|
| 61 |
_PARAM_MAP = {
|
| 62 |
"Pexit_barg": ("direct", "Pexit_barg"),
|
| 63 |
"speed_f": ("direct", "speed_f"),
|
|
@@ -78,7 +82,6 @@ _PARAM_MAP = {
|
|
| 78 |
"Exp_eff": ("proc_param", 7),
|
| 79 |
}
|
| 80 |
|
| 81 |
-
# MVP 1.0 Simplex defaults (must match engine_bridge.py)
|
| 82 |
_DEFAULTS = {
|
| 83 |
"ICVparam": [20.5, 48, 5, 779.3, 33.4, 3.75, 0.0000736, 1.0, 100],
|
| 84 |
"DCVparam": [8.3, 25, 3.79, 78.54, 7.8, 1.053, 0.0000026, 0.8, 200],
|
|
@@ -86,32 +89,43 @@ _DEFAULTS = {
|
|
| 86 |
"proc_param": [300, 10, 4, 30, 0.006, 0, 0.5, 0.2],
|
| 87 |
}
|
| 88 |
|
| 89 |
-
# HuggingFace Space caps
|
| 90 |
-
_IS_HF = bool(os.environ.get("HF_SPACE"))
|
| 91 |
-
_MAX_N_1D = 15 if _IS_HF else 20
|
| 92 |
-
_MAX_GRID_2D = 10 if _IS_HF else 15
|
| 93 |
-
|
| 94 |
-
# Module-level cache (created lazily so import doesn't touch disk)
|
| 95 |
-
_cache: SweepCache | None = None
|
| 96 |
-
|
| 97 |
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
|
| 104 |
|
| 105 |
def _build_overrides(overrides: dict[str, float]) -> dict:
|
| 106 |
-
"""
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
ICVparam, DCVparam, pump_geom, proc_param β only those that differ
|
| 110 |
-
from defaults.
|
| 111 |
-
"""
|
| 112 |
kw: dict = {}
|
| 113 |
arrays: dict[str, list] = {}
|
| 114 |
-
|
| 115 |
for name, val in overrides.items():
|
| 116 |
mapping = _PARAM_MAP[name]
|
| 117 |
if mapping[0] == "direct":
|
|
@@ -119,54 +133,114 @@ def _build_overrides(overrides: dict[str, float]) -> dict:
|
|
| 119 |
else:
|
| 120 |
arr_name, idx = mapping
|
| 121 |
if arr_name not in arrays:
|
| 122 |
-
arrays[arr_name] = list(_DEFAULTS[arr_name])
|
| 123 |
arrays[arr_name][idx] = val
|
| 124 |
-
|
| 125 |
kw.update(arrays)
|
| 126 |
return kw
|
| 127 |
|
| 128 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
# ββ Helper: run a single sim point with caching βββββββββββββββββββββββββββββ
|
| 130 |
|
| 131 |
-
def _sim_point(
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
cache = _get_cache()
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 146 |
|
| 147 |
hit = cache.get(params_key)
|
| 148 |
if hit is not None:
|
| 149 |
scalars, wall_s = hit
|
| 150 |
-
return scalars
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
all_overrides = {"Pexit_barg": Pexit, "speed_f": speed, "flash_eff": flash_eff}
|
| 154 |
-
if extra_overrides:
|
| 155 |
-
all_overrides.update(extra_overrides)
|
| 156 |
-
kw = _build_overrides(all_overrides)
|
| 157 |
|
|
|
|
| 158 |
t0 = time.perf_counter()
|
| 159 |
_out, hist = cycle_sim(engine="lightspeed", **kw)
|
| 160 |
wall_s = time.perf_counter() - t0
|
| 161 |
mdot = hist.get("mdot_kgpm", 0.0)
|
|
|
|
| 162 |
|
| 163 |
-
cache.put(params_key, {"mdot_kgpm": mdot}, wall_s)
|
| 164 |
-
return mdot, wall_s, False
|
| 165 |
|
| 166 |
|
| 167 |
# ββ Main click handler βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 168 |
|
| 169 |
def run_sweep(
|
|
|
|
| 170 |
mode: str,
|
| 171 |
hw: str,
|
| 172 |
param_1d: str,
|
|
@@ -178,14 +252,19 @@ def run_sweep(
|
|
| 178 |
grid_2d: int,
|
| 179 |
Pexit: float,
|
| 180 |
speed: float,
|
|
|
|
| 181 |
):
|
| 182 |
"""Execute a 1D or 2D sweep and return (plot, cache_html, dataframe)."""
|
|
|
|
|
|
|
| 183 |
|
| 184 |
try:
|
| 185 |
if mode == "1D Sweep":
|
| 186 |
-
return _run_1d(hw, param_1d, min_1d, max_1d,
|
|
|
|
| 187 |
else:
|
| 188 |
-
return _run_2d(hw, param_x, param_y,
|
|
|
|
| 189 |
except Exception:
|
| 190 |
err = traceback.format_exc()
|
| 191 |
err_html = (
|
|
@@ -195,10 +274,10 @@ def run_sweep(
|
|
| 195 |
)
|
| 196 |
empty_fig = go.Figure()
|
| 197 |
empty_fig.update_layout(height=300)
|
| 198 |
-
return empty_fig, err_html, pd.DataFrame()
|
| 199 |
|
| 200 |
|
| 201 |
-
def _run_1d(hw, param, lo, hi, n, Pexit, speed):
|
| 202 |
"""1D parameter sweep β supports all 17 parameters."""
|
| 203 |
n = min(n, _MAX_N_1D)
|
| 204 |
|
|
@@ -208,13 +287,11 @@ def _run_1d(hw, param, lo, hi, n, Pexit, speed):
|
|
| 208 |
cache_hits = 0
|
| 209 |
|
| 210 |
for v in values:
|
| 211 |
-
# Direct args default to operating point; overridden if swept
|
| 212 |
p = float(v) if param == "Pexit_barg" else Pexit
|
| 213 |
s = float(v) if param == "speed_f" else speed
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
mdot, wall, cached = _sim_point(hw, p, s, fe, extra)
|
| 218 |
mdots.append(mdot)
|
| 219 |
total_wall += wall
|
| 220 |
if cached:
|
|
@@ -223,13 +300,16 @@ def _run_1d(hw, param, lo, hi, n, Pexit, speed):
|
|
| 223 |
# ββ Plotly line chart ββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 224 |
label = SWEEP_PARAMS[param]["label"]
|
| 225 |
fig = go.Figure()
|
|
|
|
| 226 |
fig.add_trace(go.Scatter(
|
| 227 |
x=values,
|
| 228 |
y=mdots,
|
| 229 |
-
mode=
|
| 230 |
name="mdot",
|
| 231 |
line=dict(color=TRACE_COLORS[0], width=2),
|
| 232 |
marker=dict(size=6, color=TRACE_COLORS[0]),
|
|
|
|
|
|
|
| 233 |
))
|
| 234 |
fig.update_layout(
|
| 235 |
title=f"1D Sweep: {label}",
|
|
@@ -268,11 +348,20 @@ def _run_1d(hw, param, lo, hi, n, Pexit, speed):
|
|
| 268 |
"mdot [kg/min]": np.round(mdots, 4),
|
| 269 |
})
|
| 270 |
|
| 271 |
-
return fig, cache_html, df
|
|
|
|
| 272 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 273 |
|
| 274 |
-
def _run_2d(hw, param_x, param_y, grid_n, Pexit, speed):
|
| 275 |
-
"""2D parameter sweep β supports any two parameters from SWEEP_PARAMS."""
|
| 276 |
grid_n = min(grid_n, _MAX_GRID_2D)
|
| 277 |
|
| 278 |
if param_x == param_y:
|
|
@@ -283,7 +372,7 @@ def _run_2d(hw, param_x, param_y, grid_n, Pexit, speed):
|
|
| 283 |
)
|
| 284 |
empty_fig = go.Figure()
|
| 285 |
empty_fig.update_layout(height=300)
|
| 286 |
-
return empty_fig, info_html, pd.DataFrame()
|
| 287 |
|
| 288 |
px_info = SWEEP_PARAMS[param_x]
|
| 289 |
py_info = SWEEP_PARAMS[param_y]
|
|
@@ -291,21 +380,23 @@ def _run_2d(hw, param_x, param_y, grid_n, Pexit, speed):
|
|
| 291 |
y_vals = np.linspace(py_info["min"], py_info["max"], grid_n)
|
| 292 |
|
| 293 |
z = np.zeros((len(y_vals), len(x_vals)))
|
|
|
|
| 294 |
total_wall = 0.0
|
| 295 |
cache_hits = 0
|
| 296 |
total_pts = len(x_vals) * len(y_vals)
|
| 297 |
|
| 298 |
-
_direct_set = {"Pexit_barg", "speed_f", "flash_eff"}
|
| 299 |
-
|
| 300 |
for j, yv in enumerate(y_vals):
|
| 301 |
for i, xv in enumerate(x_vals):
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
mdot, wall, cached = _sim_point(
|
|
|
|
|
|
|
| 308 |
z[j, i] = mdot
|
|
|
|
| 309 |
total_wall += wall
|
| 310 |
if cached:
|
| 311 |
cache_hits += 1
|
|
@@ -374,11 +465,79 @@ def _run_2d(hw, param_x, param_y, grid_n, Pexit, speed):
|
|
| 374 |
})
|
| 375 |
df = pd.DataFrame(rows)
|
| 376 |
|
| 377 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 378 |
|
| 379 |
|
| 380 |
# ββ Utility ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 381 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 382 |
def _hex_to_rgb(h: str) -> str:
|
| 383 |
"""Convert '#4a9eca' to '74,158,202'."""
|
| 384 |
h = h.lstrip("#")
|
|
@@ -387,8 +546,15 @@ def _hex_to_rgb(h: str) -> str:
|
|
| 387 |
|
| 388 |
# ββ Builder ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 389 |
|
| 390 |
-
def build_sweep_tab():
|
| 391 |
-
"""Create all Gradio components for the Parameter Sweep tab
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 392 |
|
| 393 |
Must be called inside an active ``gr.TabItem(...)`` context manager.
|
| 394 |
"""
|
|
@@ -401,9 +567,16 @@ def build_sweep_tab():
|
|
| 401 |
with gr.Column(scale=1, min_width=280):
|
| 402 |
gr.HTML(engine_banner("ENGINE: LIGHTSPEED"))
|
| 403 |
|
|
|
|
|
|
|
|
|
|
| 404 |
hw_dd = gr.Dropdown(
|
| 405 |
-
choices=[
|
| 406 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 407 |
label="Hardware",
|
| 408 |
)
|
| 409 |
|
|
@@ -467,6 +640,11 @@ def build_sweep_tab():
|
|
| 467 |
label="Operating Speed Fraction",
|
| 468 |
)
|
| 469 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 470 |
run_btn = gr.Button("RUN SWEEP", variant="primary")
|
| 471 |
|
| 472 |
# ββ Right panel β results ββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -474,6 +652,9 @@ def build_sweep_tab():
|
|
| 474 |
plot_out = gr.Plot(label="Sweep Results")
|
| 475 |
cache_html = gr.HTML(label="Cache Status")
|
| 476 |
results_df = gr.Dataframe(label="Results Table")
|
|
|
|
|
|
|
|
|
|
| 477 |
|
| 478 |
# ββ Mode toggle: show/hide 1D vs 2D control groups βββββββββββββββββββ
|
| 479 |
def toggle_mode(mode):
|
|
@@ -488,6 +669,12 @@ def build_sweep_tab():
|
|
| 488 |
outputs=[grp_1d, grp_2d],
|
| 489 |
)
|
| 490 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 491 |
# ββ Auto-populate min/max when 1D parameter changes ββββββββββββββββββ
|
| 492 |
def update_minmax(param):
|
| 493 |
info = SWEEP_PARAMS.get(param, SWEEP_PARAMS[first_key])
|
|
@@ -499,14 +686,27 @@ def build_sweep_tab():
|
|
| 499 |
outputs=[min_1d, max_1d],
|
| 500 |
)
|
| 501 |
|
| 502 |
-
# ββ
|
| 503 |
-
|
| 504 |
-
fn=
|
| 505 |
-
inputs=[
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 506 |
mode_radio, hw_dd,
|
| 507 |
param_1d_dd, min_1d, max_1d, n_1d_sl,
|
| 508 |
param_x_dd, param_y_dd, grid_2d_sl,
|
| 509 |
pexit_sl, speed_sl,
|
|
|
|
| 510 |
],
|
| 511 |
-
|
| 512 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
import plotly.graph_objects as go
|
| 18 |
|
| 19 |
from murphy_unified.engine_bridge import cycle_sim
|
| 20 |
+
from murphy_unified.engine_params import build_engine_kwargs
|
| 21 |
+
from murphy_unified.sim_defaults import SIM_DEFAULTS
|
| 22 |
from murphy_unified.sweep_cache import SweepCache
|
| 23 |
from murphy_unified.theme import (
|
| 24 |
metric_html,
|
|
|
|
| 58 |
"htc": {"label": "Heat Transfer Coeff", "min": 0.0, "max": 100.0, "default": 10.0},
|
| 59 |
}
|
| 60 |
|
| 61 |
+
# ββ Legacy parameter-to-array mapping βββββββββββββββββββββββββββββββββββββ
|
| 62 |
+
# Kept for compatibility with murphy_unified/tabs/comparison.py, which still
|
| 63 |
+
# uses the old override-dict shape. New sweep flow goes through
|
| 64 |
+
# build_engine_kwargs + _SWEEP_KEY_TO_SIM_KEY below.
|
| 65 |
_PARAM_MAP = {
|
| 66 |
"Pexit_barg": ("direct", "Pexit_barg"),
|
| 67 |
"speed_f": ("direct", "speed_f"),
|
|
|
|
| 82 |
"Exp_eff": ("proc_param", 7),
|
| 83 |
}
|
| 84 |
|
|
|
|
| 85 |
_DEFAULTS = {
|
| 86 |
"ICVparam": [20.5, 48, 5, 779.3, 33.4, 3.75, 0.0000736, 1.0, 100],
|
| 87 |
"DCVparam": [8.3, 25, 3.79, 78.54, 7.8, 1.053, 0.0000026, 0.8, 200],
|
|
|
|
| 89 |
"proc_param": [300, 10, 4, 30, 0.006, 0, 0.5, 0.2],
|
| 90 |
}
|
| 91 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
|
| 93 |
+
# ββ Hardware presets ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 94 |
+
# Sourced from "ICV open sim-driver for ode package (2).xlsx" (orig vs new ICV).
|
| 95 |
+
# Applied on top of sim_state in _sim_point when the user picks a preset;
|
| 96 |
+
# "baseline" leaves sim_state unchanged.
|
| 97 |
+
_HW_PRESETS: dict[str, dict] = {
|
| 98 |
+
"old_icv": {
|
| 99 |
+
"icv_port": 19.052559,
|
| 100 |
+
"icv_mass": 28.0,
|
| 101 |
+
"icv_travel": 8.0,
|
| 102 |
+
"icv_dp_area": 897.741517,
|
| 103 |
+
"icv_Fs": 34.2,
|
| 104 |
+
"icv_SC": 3.402,
|
| 105 |
+
"icv_leakKv": 0.00669,
|
| 106 |
+
"icv_comp_eff": 1.0,
|
| 107 |
+
"icv_npts": 200,
|
| 108 |
+
},
|
| 109 |
+
"new_icv": {
|
| 110 |
+
"icv_port": 20.5,
|
| 111 |
+
"icv_mass": 48.0,
|
| 112 |
+
"icv_travel": 5.0,
|
| 113 |
+
"icv_dp_area": 779.311328,
|
| 114 |
+
"icv_Fs": 33.4,
|
| 115 |
+
"icv_SC": 3.75,
|
| 116 |
+
"icv_leakKv": 0.000864,
|
| 117 |
+
"icv_comp_eff": 1.0,
|
| 118 |
+
"icv_npts": 200,
|
| 119 |
+
},
|
| 120 |
+
}
|
| 121 |
|
| 122 |
|
| 123 |
def _build_overrides(overrides: dict[str, float]) -> dict:
|
| 124 |
+
"""Legacy helper: convert {param_name: value} to cycle_sim kwargs using
|
| 125 |
+
engine defaults for unspecified parameters. Used only by comparison tab;
|
| 126 |
+
new sweep flow goes through build_engine_kwargs."""
|
|
|
|
|
|
|
|
|
|
| 127 |
kw: dict = {}
|
| 128 |
arrays: dict[str, list] = {}
|
|
|
|
| 129 |
for name, val in overrides.items():
|
| 130 |
mapping = _PARAM_MAP[name]
|
| 131 |
if mapping[0] == "direct":
|
|
|
|
| 133 |
else:
|
| 134 |
arr_name, idx = mapping
|
| 135 |
if arr_name not in arrays:
|
| 136 |
+
arrays[arr_name] = list(_DEFAULTS[arr_name])
|
| 137 |
arrays[arr_name][idx] = val
|
|
|
|
| 138 |
kw.update(arrays)
|
| 139 |
return kw
|
| 140 |
|
| 141 |
|
| 142 |
+
# ββ Sweep-key β SIM_DEFAULTS-key aliases ββββββββββββββββββββββββββββββββββ
|
| 143 |
+
# Sweep-tab parameter names differ in some places from SIM_DEFAULTS keys (which
|
| 144 |
+
# match run_simulation signature). This table translates before applying.
|
| 145 |
+
# Params not listed here (Pexit_barg, speed_f, flash_eff, dvf, Fmult, htc) keep
|
| 146 |
+
# the same name.
|
| 147 |
+
_SWEEP_KEY_TO_SIM_KEY = {
|
| 148 |
+
"Pexit_barg": "Pexit",
|
| 149 |
+
"speed_f": "speed",
|
| 150 |
+
"Kv_BB": "KvBB",
|
| 151 |
+
"bore_mm": "bore",
|
| 152 |
+
"stroke_mm": "stroke",
|
| 153 |
+
"ICV_spring_F": "icv_Fs",
|
| 154 |
+
"DCV_spring_F": "dcv_Fs",
|
| 155 |
+
"ICV_port_dia": "icv_port",
|
| 156 |
+
"DCV_port_dia": "dcv_port",
|
| 157 |
+
"ICV_leak_Kv": "icv_leakKv",
|
| 158 |
+
"DCV_leak_Kv": "dcv_leakKv",
|
| 159 |
+
"fric2chamber": "fric",
|
| 160 |
+
"Exp_eff": "Exp",
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
def _sweep_key_to_sim_key(sweep_key: str) -> str:
|
| 165 |
+
return _SWEEP_KEY_TO_SIM_KEY.get(sweep_key, sweep_key)
|
| 166 |
+
|
| 167 |
+
# HuggingFace Space caps
|
| 168 |
+
_IS_HF = bool(os.environ.get("HF_SPACE"))
|
| 169 |
+
_MAX_N_1D = 15 if _IS_HF else 20
|
| 170 |
+
_MAX_GRID_2D = 10 if _IS_HF else 15
|
| 171 |
+
|
| 172 |
+
# Module-level cache (created lazily so import doesn't touch disk)
|
| 173 |
+
_cache: SweepCache | None = None
|
| 174 |
+
|
| 175 |
+
|
| 176 |
+
def _get_cache() -> SweepCache:
|
| 177 |
+
global _cache
|
| 178 |
+
if _cache is None:
|
| 179 |
+
_cache = SweepCache()
|
| 180 |
+
return _cache
|
| 181 |
+
|
| 182 |
+
|
| 183 |
# ββ Helper: run a single sim point with caching βββββββββββββββββββββββββββββ
|
| 184 |
|
| 185 |
+
def _sim_point(
|
| 186 |
+
sim_state: dict,
|
| 187 |
+
hw: str,
|
| 188 |
+
Pexit: float,
|
| 189 |
+
speed: float,
|
| 190 |
+
swept_param: str,
|
| 191 |
+
swept_value: float,
|
| 192 |
+
) -> tuple[float, float, float, bool]:
|
| 193 |
+
"""Run one cycle_sim call; return (mdot_kgpm, T_peak_K, wall_s, was_cached).
|
| 194 |
+
|
| 195 |
+
The full `sim_state` baseline is used β every parameter flows into
|
| 196 |
+
both the engine call AND the cache key, so stale cache rows from a
|
| 197 |
+
different baseline can never be returned.
|
| 198 |
+
"""
|
| 199 |
cache = _get_cache()
|
| 200 |
+
|
| 201 |
+
# Apply hw preset (if any) on top of baseline, then swept override.
|
| 202 |
+
sim_key = _sweep_key_to_sim_key(swept_param)
|
| 203 |
+
params = dict(sim_state)
|
| 204 |
+
params.pop("_written_at", None)
|
| 205 |
+
params.pop("_engine_ok", None)
|
| 206 |
+
if hw in _HW_PRESETS:
|
| 207 |
+
params.update(_HW_PRESETS[hw])
|
| 208 |
+
params["Pexit"] = Pexit
|
| 209 |
+
params["speed"] = speed
|
| 210 |
+
params[sim_key] = swept_value
|
| 211 |
+
|
| 212 |
+
# Cache key = full baseline + hw + engine, floats rounded for stability.
|
| 213 |
+
params_key = {"engine": "lightspeed", "hw": hw}
|
| 214 |
+
for k, v in params.items():
|
| 215 |
+
if isinstance(v, float):
|
| 216 |
+
params_key[k] = float(f"{v:.10g}")
|
| 217 |
+
elif isinstance(v, list):
|
| 218 |
+
params_key[k] = tuple(v)
|
| 219 |
+
else:
|
| 220 |
+
params_key[k] = v
|
| 221 |
|
| 222 |
hit = cache.get(params_key)
|
| 223 |
if hit is not None:
|
| 224 |
scalars, wall_s = hit
|
| 225 |
+
return (scalars.get("mdot_kgpm", 0.0),
|
| 226 |
+
scalars.get("Tc_peak_K", 60.0),
|
| 227 |
+
wall_s, True)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 228 |
|
| 229 |
+
kw = build_engine_kwargs(params)
|
| 230 |
t0 = time.perf_counter()
|
| 231 |
_out, hist = cycle_sim(engine="lightspeed", **kw)
|
| 232 |
wall_s = time.perf_counter() - t0
|
| 233 |
mdot = hist.get("mdot_kgpm", 0.0)
|
| 234 |
+
T_peak = hist.get("Tc_peak_K", 60.0)
|
| 235 |
|
| 236 |
+
cache.put(params_key, {"mdot_kgpm": mdot, "Tc_peak_K": T_peak}, wall_s)
|
| 237 |
+
return mdot, T_peak, wall_s, False
|
| 238 |
|
| 239 |
|
| 240 |
# ββ Main click handler βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 241 |
|
| 242 |
def run_sweep(
|
| 243 |
+
sim_state: dict | None,
|
| 244 |
mode: str,
|
| 245 |
hw: str,
|
| 246 |
param_1d: str,
|
|
|
|
| 252 |
grid_2d: int,
|
| 253 |
Pexit: float,
|
| 254 |
speed: float,
|
| 255 |
+
show_labels: bool = True,
|
| 256 |
):
|
| 257 |
"""Execute a 1D or 2D sweep and return (plot, cache_html, dataframe)."""
|
| 258 |
+
if sim_state is None:
|
| 259 |
+
sim_state = dict(SIM_DEFAULTS)
|
| 260 |
|
| 261 |
try:
|
| 262 |
if mode == "1D Sweep":
|
| 263 |
+
return _run_1d(sim_state, hw, param_1d, min_1d, max_1d,
|
| 264 |
+
int(n_1d), Pexit, speed, show_labels)
|
| 265 |
else:
|
| 266 |
+
return _run_2d(sim_state, hw, param_x, param_y,
|
| 267 |
+
int(grid_2d), Pexit, speed)
|
| 268 |
except Exception:
|
| 269 |
err = traceback.format_exc()
|
| 270 |
err_html = (
|
|
|
|
| 274 |
)
|
| 275 |
empty_fig = go.Figure()
|
| 276 |
empty_fig.update_layout(height=300)
|
| 277 |
+
return empty_fig, err_html, pd.DataFrame(), None, gr.update(visible=False)
|
| 278 |
|
| 279 |
|
| 280 |
+
def _run_1d(sim_state, hw, param, lo, hi, n, Pexit, speed, show_labels=True):
|
| 281 |
"""1D parameter sweep β supports all 17 parameters."""
|
| 282 |
n = min(n, _MAX_N_1D)
|
| 283 |
|
|
|
|
| 287 |
cache_hits = 0
|
| 288 |
|
| 289 |
for v in values:
|
|
|
|
| 290 |
p = float(v) if param == "Pexit_barg" else Pexit
|
| 291 |
s = float(v) if param == "speed_f" else speed
|
| 292 |
+
mdot, _T_peak, wall, cached = _sim_point(
|
| 293 |
+
sim_state, hw, p, s, param, float(v),
|
| 294 |
+
)
|
|
|
|
| 295 |
mdots.append(mdot)
|
| 296 |
total_wall += wall
|
| 297 |
if cached:
|
|
|
|
| 300 |
# ββ Plotly line chart ββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 301 |
label = SWEEP_PARAMS[param]["label"]
|
| 302 |
fig = go.Figure()
|
| 303 |
+
trace_mode = "lines+markers+text" if show_labels else "lines+markers"
|
| 304 |
fig.add_trace(go.Scatter(
|
| 305 |
x=values,
|
| 306 |
y=mdots,
|
| 307 |
+
mode=trace_mode,
|
| 308 |
name="mdot",
|
| 309 |
line=dict(color=TRACE_COLORS[0], width=2),
|
| 310 |
marker=dict(size=6, color=TRACE_COLORS[0]),
|
| 311 |
+
text=[f"{val:.4g}" for val in values] if show_labels else None,
|
| 312 |
+
textposition="top center",
|
| 313 |
))
|
| 314 |
fig.update_layout(
|
| 315 |
title=f"1D Sweep: {label}",
|
|
|
|
| 348 |
"mdot [kg/min]": np.round(mdots, 4),
|
| 349 |
})
|
| 350 |
|
| 351 |
+
return fig, cache_html, df, None, gr.update(visible=False)
|
| 352 |
+
|
| 353 |
|
| 354 |
+
def _run_2d(sim_state, hw, param_x, param_y, grid_n, Pexit, speed):
|
| 355 |
+
"""2D parameter sweep β supports any two parameters from SWEEP_PARAMS.
|
| 356 |
+
|
| 357 |
+
Returns a 5-tuple: ``(fig, cache_html, df, bridge_payload, send_btn_update)``.
|
| 358 |
+
|
| 359 |
+
``bridge_payload`` is a dict (see design spec) iff the axes are exactly
|
| 360 |
+
``{Pexit_barg, speed_f}``; otherwise ``None``. ``send_btn_update`` is a
|
| 361 |
+
``gr.update(...)`` toggling the Send-to-Process-Sim button visibility.
|
| 362 |
+
"""
|
| 363 |
+
import datetime as _dt
|
| 364 |
|
|
|
|
|
|
|
| 365 |
grid_n = min(grid_n, _MAX_GRID_2D)
|
| 366 |
|
| 367 |
if param_x == param_y:
|
|
|
|
| 372 |
)
|
| 373 |
empty_fig = go.Figure()
|
| 374 |
empty_fig.update_layout(height=300)
|
| 375 |
+
return empty_fig, info_html, pd.DataFrame(), None, gr.update(visible=False)
|
| 376 |
|
| 377 |
px_info = SWEEP_PARAMS[param_x]
|
| 378 |
py_info = SWEEP_PARAMS[param_y]
|
|
|
|
| 380 |
y_vals = np.linspace(py_info["min"], py_info["max"], grid_n)
|
| 381 |
|
| 382 |
z = np.zeros((len(y_vals), len(x_vals)))
|
| 383 |
+
z_temp = np.zeros((len(y_vals), len(x_vals)))
|
| 384 |
total_wall = 0.0
|
| 385 |
cache_hits = 0
|
| 386 |
total_pts = len(x_vals) * len(y_vals)
|
| 387 |
|
|
|
|
|
|
|
| 388 |
for j, yv in enumerate(y_vals):
|
| 389 |
for i, xv in enumerate(x_vals):
|
| 390 |
+
# Apply param_y on top of baseline; pass param_x as swept param.
|
| 391 |
+
local_state = dict(sim_state)
|
| 392 |
+
local_state[_sweep_key_to_sim_key(param_y)] = float(yv)
|
| 393 |
+
p = float(xv) if param_x == "Pexit_barg" else Pexit
|
| 394 |
+
s = float(xv) if param_x == "speed_f" else speed
|
| 395 |
+
mdot, T_peak, wall, cached = _sim_point(
|
| 396 |
+
local_state, hw, p, s, param_x, float(xv),
|
| 397 |
+
)
|
| 398 |
z[j, i] = mdot
|
| 399 |
+
z_temp[j, i] = T_peak
|
| 400 |
total_wall += wall
|
| 401 |
if cached:
|
| 402 |
cache_hits += 1
|
|
|
|
| 465 |
})
|
| 466 |
df = pd.DataFrame(rows)
|
| 467 |
|
| 468 |
+
# ββ Bridge payload (only for compatible axes) ββββββββββββββββββββββββ
|
| 469 |
+
if {param_x, param_y} == {"Pexit_barg", "speed_f"}:
|
| 470 |
+
payload = {
|
| 471 |
+
"axis_x": param_x,
|
| 472 |
+
"axis_y": param_y,
|
| 473 |
+
"x_vals": x_vals,
|
| 474 |
+
"y_vals": y_vals,
|
| 475 |
+
"z_mdot_kgpm": z,
|
| 476 |
+
"z_temp_K": z_temp,
|
| 477 |
+
"hw": hw,
|
| 478 |
+
"timestamp": _dt.datetime.now().isoformat(timespec="seconds"),
|
| 479 |
+
}
|
| 480 |
+
btn_update = gr.update(visible=True)
|
| 481 |
+
else:
|
| 482 |
+
payload = None
|
| 483 |
+
btn_update = gr.update(visible=False)
|
| 484 |
+
|
| 485 |
+
return fig, cache_html, df, payload, btn_update
|
| 486 |
|
| 487 |
|
| 488 |
# ββ Utility ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 489 |
|
| 490 |
+
_SECTIONS = [
|
| 491 |
+
("Operating",
|
| 492 |
+
["engine_name", "fluid", "Pexit", "speed", "Ptank", "Psat"]),
|
| 493 |
+
("ICV",
|
| 494 |
+
["icv_port", "icv_mass", "icv_travel", "icv_dp_area",
|
| 495 |
+
"icv_Fs", "icv_SC", "icv_leakKv", "icv_comp_eff", "icv_npts"]),
|
| 496 |
+
("DCV",
|
| 497 |
+
["dcv_port", "dcv_mass", "dcv_travel", "dcv_dp_area",
|
| 498 |
+
"dcv_Fs", "dcv_SC", "dcv_leakKv", "dcv_comp_eff", "dcv_npts"]),
|
| 499 |
+
("Pump",
|
| 500 |
+
["bore", "stroke", "hOD", "cLen", "emH", "emS",
|
| 501 |
+
"kvoid", "kH", "Vfv", "vac", "cpm", "dvf"]),
|
| 502 |
+
("Process",
|
| 503 |
+
["Tamb", "htc", "drive", "Fmult", "KvBB", "Pbb",
|
| 504 |
+
"fric", "Exp", "flash_eff"]),
|
| 505 |
+
("Downstream",
|
| 506 |
+
["fill_type", "num_cycles", "vsnubber", "vchss", "aov140f", "rodia"]),
|
| 507 |
+
]
|
| 508 |
+
|
| 509 |
+
|
| 510 |
+
def _format_baseline_panel(sim_state: dict | None) -> str:
|
| 511 |
+
"""Render sim_state as a grouped HTML table for the Sweep baseline panel."""
|
| 512 |
+
if sim_state is None:
|
| 513 |
+
sim_state = dict(SIM_DEFAULTS)
|
| 514 |
+
written_at = sim_state.get("_written_at", "defaults (no simulation run yet)")
|
| 515 |
+
engine_ok = sim_state.get("_engine_ok")
|
| 516 |
+
ok_str = ("ok" if engine_ok else "error") if engine_ok is not None else "β"
|
| 517 |
+
header = (
|
| 518 |
+
f'<div style="font-family:JetBrains Mono,monospace;font-size:11px;'
|
| 519 |
+
f'color:{TEXT_DIM};padding:4px 0;">'
|
| 520 |
+
f'Source: user inputs at {written_at} (engine: {ok_str})</div>'
|
| 521 |
+
)
|
| 522 |
+
rows = []
|
| 523 |
+
for section, keys in _SECTIONS:
|
| 524 |
+
rows.append(
|
| 525 |
+
f'<tr><td colspan="2" style="padding-top:8px;font-weight:600;'
|
| 526 |
+
f'color:{TEXT_BRIGHT};">{section}</td></tr>'
|
| 527 |
+
)
|
| 528 |
+
for key in keys:
|
| 529 |
+
val = sim_state.get(key, "")
|
| 530 |
+
rows.append(
|
| 531 |
+
f'<tr><td style="color:{TEXT_DIM};padding:2px 12px 2px 0;">{key}</td>'
|
| 532 |
+
f'<td style="font-family:JetBrains Mono,monospace;color:{TEXT_BRIGHT};">{val}</td></tr>'
|
| 533 |
+
)
|
| 534 |
+
table = (
|
| 535 |
+
'<table style="border-collapse:collapse;font-size:11px;">'
|
| 536 |
+
+ "".join(rows) + "</table>"
|
| 537 |
+
)
|
| 538 |
+
return header + table
|
| 539 |
+
|
| 540 |
+
|
| 541 |
def _hex_to_rgb(h: str) -> str:
|
| 542 |
"""Convert '#4a9eca' to '74,158,202'."""
|
| 543 |
h = h.lstrip("#")
|
|
|
|
| 546 |
|
| 547 |
# ββ Builder ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 548 |
|
| 549 |
+
def build_sweep_tab(sim_params_state: gr.State):
|
| 550 |
+
"""Create all Gradio components for the Parameter Sweep tab.
|
| 551 |
+
|
| 552 |
+
Returns a dict of handles for app.py to wire cross-tab events:
|
| 553 |
+
* ``run_btn``, ``run_sweep`` (callable)
|
| 554 |
+
* ``run_inputs`` (list of components for the click handler)
|
| 555 |
+
* ``plot_out``, ``cache_html``, ``results_df``
|
| 556 |
+
* ``send_to_process_btn``
|
| 557 |
+
* ``mode_radio``
|
| 558 |
|
| 559 |
Must be called inside an active ``gr.TabItem(...)`` context manager.
|
| 560 |
"""
|
|
|
|
| 567 |
with gr.Column(scale=1, min_width=280):
|
| 568 |
gr.HTML(engine_banner("ENGINE: LIGHTSPEED"))
|
| 569 |
|
| 570 |
+
with gr.Accordion("Baseline parameters in use", open=False):
|
| 571 |
+
baseline_html = gr.HTML(_format_baseline_panel(None))
|
| 572 |
+
|
| 573 |
hw_dd = gr.Dropdown(
|
| 574 |
+
choices=[
|
| 575 |
+
("Baseline (Simulation tab)", "baseline"),
|
| 576 |
+
("Old ICV", "old_icv"),
|
| 577 |
+
("New ICV", "new_icv"),
|
| 578 |
+
],
|
| 579 |
+
value="baseline",
|
| 580 |
label="Hardware",
|
| 581 |
)
|
| 582 |
|
|
|
|
| 640 |
label="Operating Speed Fraction",
|
| 641 |
)
|
| 642 |
|
| 643 |
+
show_labels_cb = gr.Checkbox(
|
| 644 |
+
label="Show point labels on 1D plot",
|
| 645 |
+
value=True,
|
| 646 |
+
)
|
| 647 |
+
|
| 648 |
run_btn = gr.Button("RUN SWEEP", variant="primary")
|
| 649 |
|
| 650 |
# ββ Right panel β results ββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 652 |
plot_out = gr.Plot(label="Sweep Results")
|
| 653 |
cache_html = gr.HTML(label="Cache Status")
|
| 654 |
results_df = gr.Dataframe(label="Results Table")
|
| 655 |
+
send_to_process_btn = gr.Button(
|
| 656 |
+
"Send to Process Sim", variant="secondary", visible=False,
|
| 657 |
+
)
|
| 658 |
|
| 659 |
# ββ Mode toggle: show/hide 1D vs 2D control groups βββββββββββββββββββ
|
| 660 |
def toggle_mode(mode):
|
|
|
|
| 669 |
outputs=[grp_1d, grp_2d],
|
| 670 |
)
|
| 671 |
|
| 672 |
+
mode_radio.change(
|
| 673 |
+
fn=lambda _m: gr.update(visible=False),
|
| 674 |
+
inputs=[mode_radio],
|
| 675 |
+
outputs=[send_to_process_btn],
|
| 676 |
+
)
|
| 677 |
+
|
| 678 |
# ββ Auto-populate min/max when 1D parameter changes ββββββββββββββββββ
|
| 679 |
def update_minmax(param):
|
| 680 |
info = SWEEP_PARAMS.get(param, SWEEP_PARAMS[first_key])
|
|
|
|
| 686 |
outputs=[min_1d, max_1d],
|
| 687 |
)
|
| 688 |
|
| 689 |
+
# ββ Reactive: baseline panel updates whenever sim_params_state changes ββ
|
| 690 |
+
sim_params_state.change(
|
| 691 |
+
fn=_format_baseline_panel,
|
| 692 |
+
inputs=[sim_params_state],
|
| 693 |
+
outputs=[baseline_html],
|
| 694 |
+
)
|
| 695 |
+
|
| 696 |
+
return {
|
| 697 |
+
"run_btn": run_btn,
|
| 698 |
+
"run_sweep": run_sweep,
|
| 699 |
+
"run_inputs": [
|
| 700 |
+
sim_params_state,
|
| 701 |
mode_radio, hw_dd,
|
| 702 |
param_1d_dd, min_1d, max_1d, n_1d_sl,
|
| 703 |
param_x_dd, param_y_dd, grid_2d_sl,
|
| 704 |
pexit_sl, speed_sl,
|
| 705 |
+
show_labels_cb,
|
| 706 |
],
|
| 707 |
+
"plot_out": plot_out,
|
| 708 |
+
"cache_html": cache_html,
|
| 709 |
+
"results_df": results_df,
|
| 710 |
+
"send_to_process_btn": send_to_process_btn,
|
| 711 |
+
"mode_radio": mode_radio,
|
| 712 |
+
}
|