pandeydigant31 commited on
Commit
5f5369e
Β·
verified Β·
1 Parent(s): e245d18

Batch B: #12 blowby, #1 split plots, #13 modebar, #2 export

Browse files
murphy_unified/tabs/simulation.py CHANGED
@@ -269,7 +269,7 @@ def run_simulation(
269
  )
270
  ef = _empty_fig()
271
  sim_state["_engine_ok"] = False
272
- return err_html, params_html, ef, ef, ef, ef, err_html, sim_state
273
 
274
  # ── Extract scalars (prefer hist dict) ───────────────────────────────
275
  mdot = hist.get("mdot_kgpm", 0.0)
@@ -308,6 +308,9 @@ def run_simulation(
308
  eff_accent = "green" if mass_eff > 0.5 else "amber"
309
  kWh_str = f"{kWh_ext:.3f}" if kWh_ext is not None else "N/A"
310
 
 
 
 
311
  metrics_html = (
312
  '<div class="metric-row">'
313
  + metric_html("MASS FLOW", f"{mdot:.3f}", "kg/min")
@@ -317,37 +320,38 @@ def run_simulation(
317
  + '<div class="metric-row">'
318
  + metric_html("PEAK PRESSURE", f"{pc_peak:.0f}", "barg")
319
  + metric_html("SPECIFIC ENERGY", kWh_str, "kWh/kg")
 
320
  + metric_html("CPU TIME", f"{cpu_s:.2f}", "s")
321
  + "</div>"
322
  )
323
 
324
- # ── P-T overlay plot ─────────────────────────────────────────────────
325
  angle = hist.get("angle_deg", np.array([]))
326
- fig_pt = go.Figure()
327
- fig_pt.add_trace(go.Scatter(
328
  x=angle, y=hist.get("pc", []),
329
  name="Chamber P",
330
  line=dict(color=TRACE_COLORS[0], width=1.5),
331
  ))
332
- fig_pt.add_trace(go.Scatter(
 
 
 
 
 
 
 
 
 
333
  x=angle, y=hist.get("Tc_K", []),
334
- name="Chamber T [K]",
335
- yaxis="y2",
336
  line=dict(color=TRACE_COLORS[1], width=1.5),
337
  ))
338
- fig_pt.update_layout(
339
- title=f"{actual_engine} @ {Pexit:.0f} barg, speed={speed:.2f}",
340
  xaxis_title="Crank angle [deg]",
341
- yaxis_title="Pressure [barg]",
342
- yaxis2=dict(
343
- title="Temperature [K]",
344
- overlaying="y",
345
- side="right",
346
- tickfont=dict(family=FONT_MONO, size=10,
347
- color=TRACE_COLORS[1]),
348
- ),
349
- height=380,
350
- legend=dict(x=0.01, y=0.99, bgcolor="rgba(0,0,0,0)"),
351
  )
352
 
353
  # ── Valve dynamics plot ──────────────────────────────────────────────
@@ -436,8 +440,27 @@ def run_simulation(
436
 
437
  status_html = f'<div class="status-bar">{" | ".join(parts)}</div>'
438
 
439
- return (metrics_html, params_html, fig_pt, fig_valves,
440
- extra_figs[0], extra_figs[1], status_html, sim_state)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
441
 
442
 
443
  # ── Builder ──────────────────────────────────────────────────────────────────
@@ -637,13 +660,30 @@ def build_simulation_tab(sim_params_state: gr.State):
637
  with gr.Column(scale=3):
638
  metrics_out = gr.HTML(label="Metrics")
639
  params_out = gr.HTML(label="Parameter Summary")
640
- fig_pt_out = gr.Plot(label="P-T Overlay")
 
 
641
  fig_valves_out = gr.Plot(label="Valve Dynamics")
642
  with gr.Row():
643
  fig_extra1_out = gr.Plot(label="Extra Plot 1")
644
  fig_extra2_out = gr.Plot(label="Extra Plot 2")
645
  status_out = gr.HTML(label="Status")
646
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
647
  # ── Wire click event ─────────────────────────────────────────────────
648
  run_btn.click(
649
  fn=run_simulation,
@@ -666,8 +706,9 @@ def build_simulation_tab(sim_params_state: gr.State):
666
  # Output selector
667
  extras_cb,
668
  ],
669
- outputs=[metrics_out, params_out, fig_pt_out, fig_valves_out,
670
- fig_extra1_out, fig_extra2_out, status_out, sim_params_state],
 
671
  )
672
 
673
  # ── Widget list (order must match _WIDGET_KEYS_IN_ORDER) ─────────────
@@ -712,3 +753,42 @@ def build_simulation_tab(sim_params_state: gr.State):
712
  inputs=_all_widgets,
713
  outputs=[sim_params_state],
714
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
269
  )
270
  ef = _empty_fig()
271
  sim_state["_engine_ok"] = False
272
+ return err_html, params_html, ef, ef, ef, ef, ef, err_html, sim_state, None
273
 
274
  # ── Extract scalars (prefer hist dict) ───────────────────────────────
275
  mdot = hist.get("mdot_kgpm", 0.0)
 
308
  eff_accent = "green" if mass_eff > 0.5 else "amber"
309
  kWh_str = f"{kWh_ext:.3f}" if kWh_ext is not None else "N/A"
310
 
311
+ bb_kgpm = hist.get("bb_kgpm")
312
+ bb_str = f"{bb_kgpm:.4f}" if bb_kgpm is not None else "N/A"
313
+
314
  metrics_html = (
315
  '<div class="metric-row">'
316
  + metric_html("MASS FLOW", f"{mdot:.3f}", "kg/min")
 
320
  + '<div class="metric-row">'
321
  + metric_html("PEAK PRESSURE", f"{pc_peak:.0f}", "barg")
322
  + metric_html("SPECIFIC ENERGY", kWh_str, "kWh/kg")
323
+ + metric_html("BLOWBY", bb_str, "kg/min")
324
  + metric_html("CPU TIME", f"{cpu_s:.2f}", "s")
325
  + "</div>"
326
  )
327
 
328
+ # ── Pressure plot (issue #1: separate plots, not cluttered overlay) ──
329
  angle = hist.get("angle_deg", np.array([]))
330
+ fig_pressure = go.Figure()
331
+ fig_pressure.add_trace(go.Scatter(
332
  x=angle, y=hist.get("pc", []),
333
  name="Chamber P",
334
  line=dict(color=TRACE_COLORS[0], width=1.5),
335
  ))
336
+ fig_pressure.update_layout(
337
+ title=f"Chamber Pressure β€” {actual_engine} @ {Pexit:.0f} barg",
338
+ xaxis_title="Crank angle [deg]",
339
+ yaxis_title="Pressure [barg]",
340
+ height=320,
341
+ )
342
+
343
+ # ── Temperature plot ────────────────────────────────────────────────
344
+ fig_temp = go.Figure()
345
+ fig_temp.add_trace(go.Scatter(
346
  x=angle, y=hist.get("Tc_K", []),
347
+ name="Chamber T",
 
348
  line=dict(color=TRACE_COLORS[1], width=1.5),
349
  ))
350
+ fig_temp.update_layout(
351
+ title=f"Chamber Temperature β€” {actual_engine} @ {Pexit:.0f} barg",
352
  xaxis_title="Crank angle [deg]",
353
+ yaxis_title="Temperature [K]",
354
+ height=320,
 
 
 
 
 
 
 
 
355
  )
356
 
357
  # ── Valve dynamics plot ──────────────────────────────────────────────
 
440
 
441
  status_html = f'<div class="status-bar">{" | ".join(parts)}</div>'
442
 
443
+ # Store results for export (issue #2)
444
+ results_bundle = {
445
+ "scalars": {
446
+ "mdot_kgpm": mdot,
447
+ "mass_eff": mass_eff,
448
+ "Tc_peak_K": Tc_peak,
449
+ "pc_peak_barg": pc_peak,
450
+ "kWh_extend": kWh_ext,
451
+ "bb_kgpm": hist.get("bb_kgpm"),
452
+ "cpu_s": cpu_s,
453
+ "steps": steps,
454
+ "engine": actual_engine,
455
+ },
456
+ "time_series": {k: v for k, v in hist.items()
457
+ if isinstance(v, np.ndarray)},
458
+ "params": {k: v for k, v in sim_state.items()
459
+ if not k.startswith("_")},
460
+ }
461
+
462
+ return (metrics_html, params_html, fig_pressure, fig_temp, fig_valves,
463
+ extra_figs[0], extra_figs[1], status_html, sim_state, results_bundle)
464
 
465
 
466
  # ── Builder ──────────────────────────────────────────────────────────────────
 
660
  with gr.Column(scale=3):
661
  metrics_out = gr.HTML(label="Metrics")
662
  params_out = gr.HTML(label="Parameter Summary")
663
+ with gr.Row():
664
+ fig_pressure_out = gr.Plot(label="Chamber Pressure")
665
+ fig_temp_out = gr.Plot(label="Chamber Temperature")
666
  fig_valves_out = gr.Plot(label="Valve Dynamics")
667
  with gr.Row():
668
  fig_extra1_out = gr.Plot(label="Extra Plot 1")
669
  fig_extra2_out = gr.Plot(label="Extra Plot 2")
670
  status_out = gr.HTML(label="Status")
671
 
672
+ # ── Results export (issue #2) ───────────────────────────────
673
+ with gr.Accordion("Export Results", open=False):
674
+ export_sections_cb = gr.CheckboxGroup(
675
+ choices=["Key Results", "Time Series", "Parameters"],
676
+ value=["Key Results", "Time Series"],
677
+ label="Sections to include",
678
+ )
679
+ export_results_btn = gr.DownloadButton(
680
+ "Export selected sections (CSV)",
681
+ size="sm",
682
+ )
683
+
684
+ # ── Hidden state for last run results ───────────────────────────────
685
+ sim_results_state = gr.State(None)
686
+
687
  # ── Wire click event ─────────────────────────────────────────────────
688
  run_btn.click(
689
  fn=run_simulation,
 
706
  # Output selector
707
  extras_cb,
708
  ],
709
+ outputs=[metrics_out, params_out, fig_pressure_out, fig_temp_out,
710
+ fig_valves_out, fig_extra1_out, fig_extra2_out,
711
+ status_out, sim_params_state, sim_results_state],
712
  )
713
 
714
  # ── Widget list (order must match _WIDGET_KEYS_IN_ORDER) ─────────────
 
753
  inputs=_all_widgets,
754
  outputs=[sim_params_state],
755
  )
756
+
757
+ # ── Results export handler (issue #2) ────────────────────────────────
758
+ import pandas as pd
759
+
760
+ def _export_results(results: dict | None, sections: list[str]) -> str | None:
761
+ if results is None:
762
+ return None
763
+ stamp = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S")
764
+ path = os.path.join(tempfile.gettempdir(), f"murphy_results_{stamp}.xlsx")
765
+ with pd.ExcelWriter(path, engine="openpyxl") as writer:
766
+ if "Key Results" in sections and "scalars" in results:
767
+ s = results["scalars"]
768
+ df = pd.DataFrame([
769
+ {"Metric": k, "Value": v} for k, v in s.items()
770
+ if v is not None
771
+ ])
772
+ df.to_excel(writer, sheet_name="Key Results", index=False)
773
+ if "Time Series" in sections and "time_series" in results:
774
+ ts = results["time_series"]
775
+ max_len = max((len(v) for v in ts.values()), default=0)
776
+ ts_df = pd.DataFrame({
777
+ k: np.pad(v, (0, max_len - len(v)),
778
+ constant_values=np.nan)
779
+ for k, v in ts.items()
780
+ })
781
+ ts_df.to_excel(writer, sheet_name="Time Series", index=False)
782
+ if "Parameters" in sections and "params" in results:
783
+ p = results["params"]
784
+ df = pd.DataFrame([
785
+ {"Parameter": k, "Value": str(v)} for k, v in p.items()
786
+ ])
787
+ df.to_excel(writer, sheet_name="Parameters", index=False)
788
+ return path
789
+
790
+ export_results_btn.click(
791
+ fn=_export_results,
792
+ inputs=[sim_results_state, export_sections_cb],
793
+ outputs=[export_results_btn],
794
+ )
murphy_unified/theme.py CHANGED
@@ -50,6 +50,11 @@ CSS = """
50
  --body: 'DM Sans', sans-serif;
51
  }
52
 
 
 
 
 
 
53
  /* ── body / dark-mode override ──────────────────────────────────────── */
54
  body, .dark {
55
  background: var(--bg) !important;
@@ -259,6 +264,17 @@ FORCE_DARK = """
259
  function FORCE_DARK() {
260
  document.body.classList.add('dark');
261
  document.documentElement.style.colorScheme = 'dark';
 
 
 
 
 
 
 
 
 
 
 
262
  }
263
  """
264
 
@@ -328,8 +344,26 @@ _template = go.layout.Template(
328
  yaxis=_axis,
329
  colorway=TRACE_COLORS,
330
  margin=dict(l=56, r=16, t=44, b=44),
 
 
 
 
 
 
331
  )
332
  )
333
 
 
 
 
 
 
 
 
 
 
 
 
 
334
  pio.templates["murphy"] = _template
335
  pio.templates.default = "murphy"
 
50
  --body: 'DM Sans', sans-serif;
51
  }
52
 
53
+ /* ── Plotly modebar β€” always visible on hover (issue #13) ──────────── */
54
+ .js-plotly-plot .modebar { opacity: 0.7 !important; }
55
+ .js-plotly-plot:hover .modebar { opacity: 1 !important; }
56
+ .js-plotly-plot .modebar-btn { font-size: 16px; }
57
+
58
  /* ── body / dark-mode override ──────────────────────────────────────── */
59
  body, .dark {
60
  background: var(--bg) !important;
 
264
  function FORCE_DARK() {
265
  document.body.classList.add('dark');
266
  document.documentElement.style.colorScheme = 'dark';
267
+
268
+ /* Issue #13: ensure Plotly modebar (download/copy buttons) is visible */
269
+ const observer = new MutationObserver(() => {
270
+ document.querySelectorAll('.js-plotly-plot').forEach(plot => {
271
+ if (plot._fullLayout && !plot._modebarPatched) {
272
+ Plotly.relayout(plot, {'modebar.orientation': 'v'});
273
+ plot._modebarPatched = true;
274
+ }
275
+ });
276
+ });
277
+ observer.observe(document.body, {childList: true, subtree: true});
278
  }
279
  """
280
 
 
344
  yaxis=_axis,
345
  colorway=TRACE_COLORS,
346
  margin=dict(l=56, r=16, t=44, b=44),
347
+ modebar=dict(
348
+ bgcolor="rgba(0,0,0,0)",
349
+ color=TEXT_DIM,
350
+ activecolor=ACCENT,
351
+ orientation="v",
352
+ ),
353
  )
354
  )
355
 
356
+ # Plotly config applied to all figures β€” ensures modebar with download/copy
357
+ # button is visible (issue #13).
358
+ PLOTLY_CONFIG = {
359
+ "displayModeBar": True,
360
+ "modeBarButtonsToAdd": ["toImage"],
361
+ "toImageButtonOptions": {
362
+ "format": "png",
363
+ "filename": "murphy_plot",
364
+ "scale": 2,
365
+ },
366
+ }
367
+
368
  pio.templates["murphy"] = _template
369
  pio.templates.default = "murphy"