Note
Go to the end to download the full example code.
Interactive Dash Dashboard for VedEf Operating Window Exploration.
This dashboard provides real-time interactive exploration of the 5D parameter space with constraint visualization and model connectivity graph.
Usage:
python dashboard_operating_window.py
Then open browser to http://127.0.0.1:8050/
Features:
Left Panel: 5 parameter sliders with real-time model evaluation
Top Center: Interactive Cytoscape graph of model connectivity
Bottom Center: Temporal plots on node click
Right Panel: Operating window slices with current point overlay
import importlib.util
from pathlib import Path
import dash_cytoscape as cyto
import numpy as np
import plotly.graph_objs as go
from dash import Dash, Input, Output, dcc, html
from paroto.viz import get_cytoscape_stylesheet, model_graph_to_cytoscape
# Import setup_vedef_problem from the numbered file
spec = importlib.util.spec_from_file_location(
"problem_module", Path(__file__).parent / "1_setup_vedef_problem.py"
)
problem_module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(problem_module)
setup_vedef_problem = problem_module.setup_vedef_problem
# Initialize Dash app
app = Dash(__name__)
app.title = "VedEf Operating Window Explorer"
# Define parameter ranges
param_ranges = {
"V": (10e3, 60e3, "V", "kV", 1e-3), # (min, max, unit, display_unit, scale)
"e": (0.005, 0.025, "m", "mm", 1e3),
"d_t": (0.015, 0.035, "m", "mm", 1e3),
"f": (10e3, 200e3, "Hz", "kHz", 1e-3),
"E_p": (1e-3, 30e-3, "J", "mJ", 1e3),
}
# Create Cytoscape elements
parameters = ["V", "e", "d_t", "f", "E_p"]
models = [
"Initial Temperature",
"Breakdown Voltage",
"Arc Coverage",
"Arc Density",
"HV Generator",
"Flow Model",
]
constraints = [
"Breakdown",
"HV Window",
"Coverage",
"Density",
"Power",
]
cyto_elements = model_graph_to_cytoscape(parameters, models, constraints)
# App layout
app.layout = html.Div(
[
html.H1(
"VedEf Operating Window Explorer", style={"textAlign": "center", "marginBottom": 20}
),
html.Div(
[
# Left Panel: Parameter Controls
html.Div(
[
html.H3("Parameters", style={"textAlign": "center"}),
html.Hr(),
# V slider
html.Label("High Voltage V (kV)", style={"fontWeight": "bold"}),
dcc.Slider(
id="slider-V",
min=param_ranges["V"][0],
max=param_ranges["V"][1],
value=20e3,
marks={int(v): f"{v / 1e3:.0f}" for v in np.linspace(10e3, 60e3, 6)},
tooltip={"placement": "bottom", "always_visible": True},
),
html.Br(),
# e slider
html.Label("Gap Distance e (mm)", style={"fontWeight": "bold"}),
dcc.Slider(
id="slider-e",
min=param_ranges["e"][0],
max=param_ranges["e"][1],
value=0.01,
marks={v: f"{v * 1e3:.0f}" for v in np.linspace(0.005, 0.025, 5)},
tooltip={"placement": "bottom", "always_visible": True},
),
html.Br(),
# d_t slider
html.Label("Torch Diameter d_t (mm)", style={"fontWeight": "bold"}),
dcc.Slider(
id="slider-d_t",
min=param_ranges["d_t"][0],
max=param_ranges["d_t"][1],
value=0.02,
marks={v: f"{v * 1e3:.0f}" for v in np.linspace(0.015, 0.035, 5)},
tooltip={"placement": "bottom", "always_visible": True},
),
html.Br(),
# f slider
html.Label("Pulse Frequency f (kHz)", style={"fontWeight": "bold"}),
dcc.Slider(
id="slider-f",
min=param_ranges["f"][0],
max=param_ranges["f"][1],
value=50e3,
marks={int(v): f"{v / 1e3:.0f}" for v in np.logspace(4, 5.3, 6)},
tooltip={"placement": "bottom", "always_visible": True},
),
html.Br(),
# E_p slider
html.Label("Energy per Pulse E_p (mJ)", style={"fontWeight": "bold"}),
dcc.Slider(
id="slider-E_p",
min=param_ranges["E_p"][0],
max=param_ranges["E_p"][1],
value=10e-3,
marks={v: f"{v * 1e3:.0f}" for v in np.linspace(1e-3, 30e-3, 6)},
tooltip={"placement": "bottom", "always_visible": True},
),
html.Br(),
html.Hr(),
html.H4("Constraint Status", style={"textAlign": "center"}),
html.Div(id="constraint-status", style={"fontSize": 12}),
],
style={
"width": "20%",
"display": "inline-block",
"verticalAlign": "top",
"padding": 20,
"backgroundColor": "#f8f9fa",
},
),
# Center Panel: Model Graph and Plots
html.Div(
[
# Cytoscape Graph
html.H3("Model Connectivity", style={"textAlign": "center"}),
cyto.Cytoscape(
id="cytoscape-graph",
elements=cyto_elements,
style={"width": "100%", "height": "400px", "border": "1px solid #ddd"},
stylesheet=get_cytoscape_stylesheet(),
layout={"name": "breadthfirst", "directed": True, "spacingFactor": 1.5},
),
html.Hr(),
# Temporal Plot
html.H3("Temporal Behavior", style={"textAlign": "center"}),
html.Div(
id="selected-node-info", style={"textAlign": "center", "fontSize": 14}
),
dcc.Graph(id="temporal-plot", style={"height": "400px"}),
],
style={
"width": "50%",
"display": "inline-block",
"verticalAlign": "top",
"padding": 20,
},
),
# Right Panel: Operating Window Slice
html.Div(
[
html.H3("Operating Window", style={"textAlign": "center"}),
html.Label("Select 2D Slice:", style={"fontWeight": "bold"}),
dcc.Dropdown(
id="slice-selector",
options=[
{"label": "V vs e", "value": "V-e"},
{"label": "V vs f", "value": "V-f"},
{"label": "f vs E_p", "value": "f-E_p"},
{"label": "e vs d_t", "value": "e-d_t"},
],
value="V-e",
),
dcc.Graph(id="operating-window-slice", style={"height": "500px"}),
html.Div(
id="feasibility-indicator",
style={
"textAlign": "center",
"fontSize": 16,
"fontWeight": "bold",
"marginTop": 10,
},
),
],
style={
"width": "28%",
"display": "inline-block",
"verticalAlign": "top",
"padding": 20,
},
),
]
),
# Hidden div to store current evaluation results
dcc.Store(id="current-results"),
]
)
@app.callback(
[
Output("current-results", "data"),
Output("constraint-status", "children"),
Output("feasibility-indicator", "children"),
],
[
Input("slider-V", "value"),
Input("slider-e", "value"),
Input("slider-d_t", "value"),
Input("slider-f", "value"),
Input("slider-E_p", "value"),
],
)
def evaluate_model(V, e, d_t, f, E_p):
"""Evaluate OpenMDAO model at current parameter values."""
try:
# Create and setup problem
prob = setup_vedef_problem(target_power=25000.0, max_energy_density=1e9, as_subsystem=False)
# Set parameters (using ARC002 naming convention)
prob.set_val("G_UMAX_OUT", V, units="V")
prob.set_val("G_e", e, units="m")
prob.set_val("TP_D_OUT", d_t, units="m")
prob.set_val("G_F", f, units="Hz")
prob.set_val("G_Ep", E_p, units="J")
# Set fixed parameters
prob.set_val("pulse_duration", 1e-6, units="s")
prob.set_val("TP_QM", 160.0 / 86400.0, units="kg/s")
prob.set_val("ThermalDiameterModel.gas_density", 0.717, units="kg/m**3")
prob.set_val("ThermalDiameterModel.gas_heat_capacity", 2200.0, units="J/(kg*K)")
prob.set_val("ThermalDiameterModel.thermal_conductivity", 0.08, units="W/(m*K)")
prob.set_val("InitialTemperatureModel.gas_density", 0.717, units="kg/m**3")
prob.set_val("BreakdownModel.gas_properties_pressure", 101325.0, units="Pa")
# Run model
prob.run_model()
# Extract results
results = {
"T_0": float(prob.get_val("T_0", units="K")[0]),
"T_max": float(prob.get_val("T_max", units="K")[0]),
"breakdown_margin": float(prob.get_val("breakdown_margin", units="V")[0]),
"coverage_margin": float(prob.get_val("coverage_margin")[0]),
"density_margin": float(prob.get_val("density_margin", units="J/m**3")[0]),
"power_error": float(prob.get_val("PowerConstraint.power_error")[0]),
"G_PW_OUT": float(prob.get_val("G_PW_OUT", units="W")[0]),
}
# Check constraints
breakdown_ok = results["breakdown_margin"] > 0
coverage_ok = results["coverage_margin"] > 0
density_ok = results["density_margin"] > 0
power_ok = results["power_error"] < 0.1
all_ok = breakdown_ok and coverage_ok and density_ok and power_ok
# Create constraint status display
status_lines = [
html.Div(
[
html.Span(
"✓ " if breakdown_ok else "✗ ",
style={"color": "green" if breakdown_ok else "red"},
),
f"Breakdown: {results['breakdown_margin'] / 1e3:.1f} kV",
]
),
html.Div(
[
html.Span(
"✓ " if coverage_ok else "✗ ",
style={"color": "green" if coverage_ok else "red"},
),
f"Coverage: {results['coverage_margin']:.3f}",
]
),
html.Div(
[
html.Span(
"✓ " if density_ok else "✗ ",
style={"color": "green" if density_ok else "red"},
),
f"Density: {results['density_margin']:.2e} J/m³",
]
),
html.Div(
[
html.Span(
"✓ " if power_ok else "✗ ", style={"color": "green" if power_ok else "red"}
),
f"Power: {results['G_PW_OUT'] / 1e3:.1f} kW "
f"(err: {results['power_error'] * 100:.1f}%)",
]
),
]
# Feasibility indicator
if all_ok:
feas_indicator = html.Div("✓ FEASIBLE", style={"color": "green"})
else:
feas_indicator = html.Div("✗ INFEASIBLE", style={"color": "red"})
return results, status_lines, feas_indicator
except Exception as e:
return (
{},
html.Div(f"Error: {str(e)}", style={"color": "red"}),
html.Div("ERROR", style={"color": "red"}),
)
@app.callback(
[Output("temporal-plot", "figure"), Output("selected-node-info", "children")],
[Input("cytoscape-graph", "tapNodeData"), Input("current-results", "data")],
)
def update_temporal_plot(node_data, results):
"""Update temporal plot based on selected node."""
if not node_data or not results:
return go.Figure(), "Click a model node to see temporal behavior"
node_label = node_data.get("label", "")
if "Initial Temperature" in node_label:
# Plot T(t) over time
t_off = 1.0 / 50e3 # Approximate off-time
t = np.linspace(0, t_off * 5, 200)
T_amb = 300.0
delta_T = results["T_max"] - T_amb
tau = 20e-6 # Approximate relaxation time
T = T_amb + delta_T * np.exp(-t / tau)
fig = go.Figure()
fig.add_trace(go.Scatter(x=t * 1e6, y=T, mode="lines", name="Temperature"))
fig.update_layout(
title="Temperature Evolution Over Time",
xaxis_title="Time (μs)",
yaxis_title="Temperature (K)",
hovermode="x unified",
)
info = f"Showing: {node_label} temporal behavior"
else:
# Default placeholder
fig = go.Figure()
fig.add_annotation(
text="Temporal plot for this model<br>coming soon",
xref="paper",
yref="paper",
x=0.5,
y=0.5,
showarrow=False,
font=dict(size=16),
)
info = f"Selected: {node_label}"
return fig, info
@app.callback(
Output("operating-window-slice", "figure"),
[
Input("slice-selector", "value"),
Input("slider-V", "value"),
Input("slider-e", "value"),
Input("slider-d_t", "value"),
Input("slider-f", "value"),
Input("slider-E_p", "value"),
],
)
def update_operating_window(slice_type, V, e, d_t, f, E_p):
"""Update operating window slice plot."""
# Parse slice type
param_x, param_y = slice_type.split("-")
# Current point
current_vals = {"V": V, "e": e, "d_t": d_t, "f": f, "E_p": E_p}
fig = go.Figure()
# Add current point marker
fig.add_trace(
go.Scatter(
x=[current_vals[param_x]],
y=[current_vals[param_y]],
mode="markers",
marker=dict(size=15, color="red", symbol="star"),
name="Current Point",
)
)
# Set axis labels
x_label = f"{param_x} ({param_ranges[param_x][3]})" if param_x in param_ranges else param_x
y_label = f"{param_y} ({param_ranges[param_y][3]})" if param_y in param_ranges else param_y
fig.update_layout(
title=f"Operating Window: {param_x} vs {param_y}",
xaxis_title=x_label,
yaxis_title=y_label,
hovermode="closest",
showlegend=True,
)
return fig
if __name__ == "__main__":
print("\n" + "=" * 70)
print(" VedEf Operating Window Dashboard")
print("=" * 70)
print("\nStarting Dash server...")
print("Open browser to: http://127.0.0.1:8050/")
print("\nPress Ctrl+C to stop")
print("=" * 70 + "\n")
app.run_server(debug=True, host="127.0.0.1", port=8050)