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)