{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "6100d3c3-bc84-4507-a9c4-0616f48f15c0",
   "metadata": {},
   "source": [
    "# Build a simple waveguide mode solver GUI\n",
    "\n",
    "Tidy3D offers a versatile web-based [graphical user interface](tidy3d.simulation.cloud) (GUI) suitable for a wide range of FDTD simulations from photonic integrated circuit components to metasurfaces and diffractive gratings. While this general-purpose GUI is beneficial for its extensive capabilities, there are situations where you might need to focus on simulating a specific device type repeatedly. For these scenarios, Tidy3D's Python API paired with open-source Python libraries such as `tkinter` enables the easy creation of a customized GUI. This specialized interface, designed with fewer controls and buttons, streamlines the simulation process for ease of use. By sharing this bespoke GUI with less experienced colleagues, they can also efficiently run simulations without extensive prior knowledge or expertise in FDTD.\n",
    "\n",
    "<img src=\"img/mode_solver_gui.png\" width=\"700\" alt=\"GUI\">\n",
    "\n",
    "This notebook uses the `tkinter` library to build an interactive graphical interface for quickly analyzing and visualizing optical waveguide modes using the Tidy3D mode solver. It provides a quick way to run mode analysis on common waveguide configurations. The supported functionalities, features, and limitations are described below:\n",
    "\n",
    "## Supported Waveguide Types\n",
    "\n",
    "* Strip waveguide\n",
    "* Rib waveguide\n",
    "* Slot waveguide\n",
    "\n",
    "## Key Features\n",
    "\n",
    "* Interactive parameter adjustment for each waveguide type\n",
    "* Real-time visualization of waveguide geometry\n",
    "* Support for both straight and bent waveguides\n",
    "* Configurable material properties (core, cladding, and box layer indices). Currently only limit to lossless materials.\n",
    "* Adjustable simulation parameters:\n",
    "   * Wavelength\n",
    "   * Grid resolution (minimum steps per wavelength)\n",
    "   * Number of modes to solve\n",
    "   * Target effective index (optional)\n",
    "   * Bend radius (optional)\n",
    "   * PML (Perfectly Matched Layer) boundaries (optional)\n",
    "\n",
    "## Adjustable Parameters\n",
    "\n",
    "### Geometric Parameters\n",
    "\n",
    "* Core width and thickness\n",
    "* Sidewall angle\n",
    "* Slab thickness (for rib waveguides)\n",
    "* Slot gap (for slot waveguides)\n",
    "* Cladding and box layer thicknesses\n",
    "\n",
    "### Material Parameters\n",
    "\n",
    "* Core refractive index\n",
    "* Cladding refractive index\n",
    "* Box layer refractive index\n",
    "\n",
    "### Simulation Settings\n",
    "\n",
    "* Grid resolution\n",
    "* Number of modes\n",
    "* Target effective index\n",
    "* Bend radius\n",
    "* PML boundaries\n",
    "\n",
    "## Limitations\n",
    "\n",
    "This tool is designed to be minimalistic and focuses on basic waveguide mode solving. Current limitations include:\n",
    "\n",
    "### Material Properties\n",
    "\n",
    "1. Only real refractive indices are supported\n",
    "2. Materials must be isotropic\n",
    "\n",
    "### Simulation Constraints\n",
    "\n",
    "1. Mode solving at a single wavelength only\n",
    "2. Cannot include substrate or more complex geometries and configurations\n",
    "\n",
    "These features can be added in the future. For more advanced functionalities, please use the Tidy3D web GUI or Python API directly.\n",
    "\n",
    "___"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b8f136f0-1d4f-44a4-899e-462511de38bb",
   "metadata": {},
   "source": [
    "## Script\n",
    "\n",
    "Below is the script to generate and launch the GUI. "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a70657df-36a9-419f-b9b8-8b825e3c6fae",
   "metadata": {},
   "outputs": [],
   "source": [
    "import math\n",
    "import tkinter as tk\n",
    "import traceback\n",
    "from tkinter import messagebox, ttk\n",
    "\n",
    "import matplotlib.pyplot as plt\n",
    "from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg\n",
    "from matplotlib.figure import Figure\n",
    "from tidy3d import Medium, ModeSpec\n",
    "from tidy3d.plugins.mode.web import run as run_mode_solver\n",
    "from tidy3d.plugins.waveguide import RectangularDielectric\n",
    "\n",
    "\n",
    "class WaveguideGUI:\n",
    "    def __init__(self, root):\n",
    "        # Initialize the root window and set the title\n",
    "        self.root = root\n",
    "        self.root.title(\"Tidy3D Waveguide Designer\")\n",
    "\n",
    "        # Create the main frame that holds everything\n",
    "        self.main_frame = ttk.Frame(root)\n",
    "        self.main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)\n",
    "\n",
    "        # Left frame for parameter inputs\n",
    "        self.left_frame = ttk.Frame(self.main_frame)\n",
    "        self.left_frame.pack(side=tk.LEFT, fill=tk.Y, padx=5)\n",
    "\n",
    "        # Right frame for mode solver visualization\n",
    "        self.right_frame = ttk.Frame(self.main_frame)\n",
    "        self.right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=5)\n",
    "\n",
    "        # Create a Matplotlib figure and canvas for displaying the waveguide cross-section\n",
    "        self.fig = Figure(figsize=(6, 4))\n",
    "        self.ax = self.fig.add_subplot(111)\n",
    "        self.canvas = FigureCanvasTkAgg(self.fig, master=self.right_frame)\n",
    "        self.canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True)\n",
    "\n",
    "        # -----------------------------------\n",
    "        # Waveguide Type Selection\n",
    "        # -----------------------------------\n",
    "        self.type_frame = ttk.LabelFrame(self.left_frame, text=\"Waveguide Type\")\n",
    "        self.type_frame.pack(fill=tk.X, padx=5, pady=5)\n",
    "\n",
    "        # Dropdown to select waveguide type (Strip, Rib, Slot)\n",
    "        self.waveguide_type_var = tk.StringVar(value=\"Strip waveguide\")\n",
    "        self.type_combobox = ttk.Combobox(\n",
    "            self.type_frame,\n",
    "            textvariable=self.waveguide_type_var,\n",
    "            values=[\"Strip waveguide\", \"Rib waveguide\", \"Slot waveguide\"],\n",
    "            state=\"readonly\",\n",
    "        )\n",
    "        self.type_combobox.pack(fill=tk.X, padx=5, pady=5)\n",
    "        self.type_combobox.bind(\"<<ComboboxSelected>>\", lambda e: self._on_type_change())\n",
    "\n",
    "        # -----------------------------------\n",
    "        # Parameter Frames for Each Waveguide Type\n",
    "        # -----------------------------------\n",
    "\n",
    "        # Parameters for Strip waveguide\n",
    "        self.strip_frame = ttk.LabelFrame(self.left_frame, text=\"Strip Waveguide Parameters\")\n",
    "        self.strip_frame.pack(fill=tk.X, padx=5, pady=5)\n",
    "\n",
    "        ttk.Label(self.strip_frame, text=\"Core Width (um):\").grid(row=0, column=0, padx=5, pady=5)\n",
    "        self.core_width_var = tk.DoubleVar(value=0.5)\n",
    "        ttk.Entry(self.strip_frame, textvariable=self.core_width_var).grid(\n",
    "            row=0, column=1, padx=5, pady=5\n",
    "        )\n",
    "\n",
    "        ttk.Label(self.strip_frame, text=\"Core Thickness (um):\").grid(\n",
    "            row=1, column=0, padx=5, pady=5\n",
    "        )\n",
    "        self.core_thickness_var = tk.DoubleVar(value=0.22)\n",
    "        ttk.Entry(self.strip_frame, textvariable=self.core_thickness_var).grid(\n",
    "            row=1, column=1, padx=5, pady=5\n",
    "        )\n",
    "\n",
    "        ttk.Label(self.strip_frame, text=\"Sidewall Angle (deg):\").grid(\n",
    "            row=2, column=0, padx=5, pady=5\n",
    "        )\n",
    "        self.sidewall_angle_var = tk.DoubleVar(value=10.0)\n",
    "        ttk.Entry(self.strip_frame, textvariable=self.sidewall_angle_var).grid(\n",
    "            row=2, column=1, padx=5, pady=5\n",
    "        )\n",
    "\n",
    "        # Parameters for Rib waveguide (initially hidden)\n",
    "        self.rib_frame = ttk.LabelFrame(self.left_frame, text=\"Rib Waveguide Parameters\")\n",
    "\n",
    "        ttk.Label(self.rib_frame, text=\"Core Width (um):\").grid(row=0, column=0, padx=5, pady=5)\n",
    "        self.rib_width_var = tk.DoubleVar(value=0.5)\n",
    "        ttk.Entry(self.rib_frame, textvariable=self.rib_width_var).grid(\n",
    "            row=0, column=1, padx=5, pady=5\n",
    "        )\n",
    "\n",
    "        ttk.Label(self.rib_frame, text=\"Core Thickness (um):\").grid(row=1, column=0, padx=5, pady=5)\n",
    "        self.rib_thickness_var = tk.DoubleVar(value=0.22)\n",
    "        ttk.Entry(self.rib_frame, textvariable=self.rib_thickness_var).grid(\n",
    "            row=1, column=1, padx=5, pady=5\n",
    "        )\n",
    "\n",
    "        ttk.Label(self.rib_frame, text=\"Sidewall Angle (deg):\").grid(\n",
    "            row=2, column=0, padx=5, pady=5\n",
    "        )\n",
    "        self.rib_angle_var = tk.DoubleVar(value=10.0)\n",
    "        ttk.Entry(self.rib_frame, textvariable=self.rib_angle_var).grid(\n",
    "            row=2, column=1, padx=5, pady=5\n",
    "        )\n",
    "\n",
    "        ttk.Label(self.rib_frame, text=\"Slab Thickness (um):\").grid(row=3, column=0, padx=5, pady=5)\n",
    "        self.slab_thickness_var = tk.DoubleVar(value=0.1)\n",
    "        ttk.Entry(self.rib_frame, textvariable=self.slab_thickness_var).grid(\n",
    "            row=3, column=1, padx=5, pady=5\n",
    "        )\n",
    "\n",
    "        # Parameters for Slot waveguide (initially hidden)\n",
    "        self.slot_frame = ttk.LabelFrame(self.left_frame, text=\"Slot Waveguide Parameters\")\n",
    "\n",
    "        ttk.Label(self.slot_frame, text=\"First Core Width (um):\").grid(\n",
    "            row=0, column=0, padx=5, pady=5\n",
    "        )\n",
    "        self.first_core_width_var = tk.DoubleVar(value=0.5)\n",
    "        ttk.Entry(self.slot_frame, textvariable=self.first_core_width_var).grid(\n",
    "            row=0, column=1, padx=5, pady=5\n",
    "        )\n",
    "\n",
    "        ttk.Label(self.slot_frame, text=\"Second Core Width (um):\").grid(\n",
    "            row=1, column=0, padx=5, pady=5\n",
    "        )\n",
    "        self.second_core_width_var = tk.DoubleVar(value=0.5)\n",
    "        ttk.Entry(self.slot_frame, textvariable=self.second_core_width_var).grid(\n",
    "            row=1, column=1, padx=5, pady=5\n",
    "        )\n",
    "\n",
    "        ttk.Label(self.slot_frame, text=\"Gap (um):\").grid(row=2, column=0, padx=5, pady=5)\n",
    "        self.gap_var = tk.DoubleVar(value=0.1)\n",
    "        ttk.Entry(self.slot_frame, textvariable=self.gap_var).grid(row=2, column=1, padx=5, pady=5)\n",
    "\n",
    "        ttk.Label(self.slot_frame, text=\"Core Thickness (um):\").grid(\n",
    "            row=3, column=0, padx=5, pady=5\n",
    "        )\n",
    "        self.slot_thickness_var = tk.DoubleVar(value=0.22)\n",
    "        ttk.Entry(self.slot_frame, textvariable=self.slot_thickness_var).grid(\n",
    "            row=3, column=1, padx=5, pady=5\n",
    "        )\n",
    "\n",
    "        ttk.Label(self.slot_frame, text=\"Sidewall Angle (deg):\").grid(\n",
    "            row=4, column=0, padx=5, pady=5\n",
    "        )\n",
    "        self.slot_angle_var = tk.DoubleVar(value=10.0)\n",
    "        ttk.Entry(self.slot_frame, textvariable=self.slot_angle_var).grid(\n",
    "            row=4, column=1, padx=5, pady=5\n",
    "        )\n",
    "\n",
    "        # -----------------------------------\n",
    "        # Common Parameters (Used by all Waveguide Types)\n",
    "        # -----------------------------------\n",
    "        self.common_frame = ttk.LabelFrame(self.left_frame, text=\"Common Parameters\")\n",
    "        self.common_frame.pack(fill=tk.X, padx=5, pady=5)\n",
    "\n",
    "        # Core, Cladding, and Box indices and thicknesses\n",
    "        ttk.Label(self.common_frame, text=\"Core Index:\").grid(row=0, column=0, padx=5, pady=5)\n",
    "        self.core_index_var = tk.DoubleVar(value=3.47)\n",
    "        ttk.Entry(self.common_frame, textvariable=self.core_index_var).grid(\n",
    "            row=0, column=1, padx=5, pady=5\n",
    "        )\n",
    "\n",
    "        ttk.Label(self.common_frame, text=\"Clad Index:\").grid(row=1, column=0, padx=5, pady=5)\n",
    "        self.clad_index_var = tk.DoubleVar(value=1.0)\n",
    "        ttk.Entry(self.common_frame, textvariable=self.clad_index_var).grid(\n",
    "            row=1, column=1, padx=5, pady=5\n",
    "        )\n",
    "\n",
    "        ttk.Label(self.common_frame, text=\"Box Index:\").grid(row=2, column=0, padx=5, pady=5)\n",
    "        self.box_index_var = tk.DoubleVar(value=1.44)\n",
    "        ttk.Entry(self.common_frame, textvariable=self.box_index_var).grid(\n",
    "            row=2, column=1, padx=5, pady=5\n",
    "        )\n",
    "\n",
    "        ttk.Label(self.common_frame, text=\"Clad Thickness (um):\").grid(\n",
    "            row=3, column=0, padx=5, pady=5\n",
    "        )\n",
    "        self.clad_thickness_var = tk.DoubleVar(value=2.0)\n",
    "        ttk.Entry(self.common_frame, textvariable=self.clad_thickness_var).grid(\n",
    "            row=3, column=1, padx=5, pady=5\n",
    "        )\n",
    "\n",
    "        ttk.Label(self.common_frame, text=\"Box Thickness (um):\").grid(\n",
    "            row=4, column=0, padx=5, pady=5\n",
    "        )\n",
    "        self.box_thickness_var = tk.DoubleVar(value=2.0)\n",
    "        ttk.Entry(self.common_frame, textvariable=self.box_thickness_var).grid(\n",
    "            row=4, column=1, padx=5, pady=5\n",
    "        )\n",
    "\n",
    "        # Wavelength, Grid resolution, and number of modes for the simulation\n",
    "        ttk.Label(self.common_frame, text=\"Wavelength (um):\").grid(row=5, column=0, padx=5, pady=5)\n",
    "        self.wavelength_var = tk.DoubleVar(value=1.55)\n",
    "        ttk.Entry(self.common_frame, textvariable=self.wavelength_var).grid(\n",
    "            row=5, column=1, padx=5, pady=5\n",
    "        )\n",
    "\n",
    "        ttk.Label(self.common_frame, text=\"Grid Resolution:\").grid(row=6, column=0, padx=5, pady=5)\n",
    "        self.grid_resolution_var = tk.DoubleVar(value=25)\n",
    "        ttk.Entry(self.common_frame, textvariable=self.grid_resolution_var).grid(\n",
    "            row=6, column=1, padx=5, pady=5\n",
    "        )\n",
    "\n",
    "        ttk.Label(self.common_frame, text=\"Number of Modes:\").grid(row=7, column=0, padx=5, pady=5)\n",
    "        self.num_modes_var = tk.IntVar(value=1)\n",
    "        ttk.Entry(self.common_frame, textvariable=self.num_modes_var).grid(\n",
    "            row=7, column=1, padx=5, pady=5\n",
    "        )\n",
    "\n",
    "        # Optional parameters: Target n_eff and Bend Radius\n",
    "        ttk.Label(self.common_frame, text=\"Target n_eff:\").grid(row=8, column=0, padx=5, pady=5)\n",
    "        self.target_neff_entry = ttk.Entry(\n",
    "            self.common_frame,\n",
    "            width=10,\n",
    "            validate=\"key\",\n",
    "            validatecommand=(self.root.register(self._validate_float_or_empty), \"%P\"),\n",
    "        )\n",
    "        self.target_neff_entry.grid(row=8, column=1, padx=5, pady=5)\n",
    "\n",
    "        vcmd = (self.root.register(self._validate_float_or_empty), \"%P\")\n",
    "        ttk.Label(self.common_frame, text=\"Bend Radius (um):\").grid(row=9, column=0, padx=5, pady=5)\n",
    "        self.bend_radius_entry = ttk.Entry(\n",
    "            self.common_frame, width=10, validate=\"key\", validatecommand=vcmd\n",
    "        )\n",
    "        self.bend_radius_entry.grid(row=9, column=1, padx=5, pady=5)\n",
    "\n",
    "        # PML (Perfectly Matched Layer) usage toggle\n",
    "        ttk.Label(self.common_frame, text=\"Use PML:\").grid(row=10, column=0, padx=5, pady=5)\n",
    "        self.use_pml_var = tk.StringVar(value=\"False\")\n",
    "        pml_combo = ttk.Combobox(\n",
    "            self.common_frame,\n",
    "            textvariable=self.use_pml_var,\n",
    "            values=[\"True\", \"False\"],\n",
    "            width=7,\n",
    "            state=\"readonly\",\n",
    "        )\n",
    "        pml_combo.grid(row=10, column=1, padx=5, pady=5)\n",
    "        pml_combo.set(\"False\")\n",
    "\n",
    "        # -----------------------------------\n",
    "        # Simulation Parameters Section (Currently empty, can be expanded in future)\n",
    "        # -----------------------------------\n",
    "        self.sim_frame = ttk.LabelFrame(self.left_frame, text=\"Simulation Parameters\")\n",
    "        self.sim_frame.pack(fill=tk.X, padx=5, pady=5)\n",
    "\n",
    "        # -----------------------------------\n",
    "        # Buttons for Actions (Plot, Solve Locally, Solve on Server)\n",
    "        # -----------------------------------\n",
    "        self.button_frame = ttk.Frame(self.left_frame)\n",
    "        self.button_frame.pack(fill=tk.X, padx=5, pady=5)\n",
    "\n",
    "        # Button to update/plot the cross-section\n",
    "        ttk.Button(self.button_frame, text=\"Plot\", command=self._update_plot).pack(\n",
    "            side=tk.LEFT, padx=5\n",
    "        )\n",
    "\n",
    "        # Frame to hold solve buttons\n",
    "        solve_frame = ttk.Frame(self.button_frame)\n",
    "        solve_frame.pack(side=tk.LEFT, padx=5)\n",
    "\n",
    "        # Solve locally (on the user's machine)\n",
    "        ttk.Button(solve_frame, text=\"Local mode solve\", command=self._solve_local_mode).pack(\n",
    "            side=tk.LEFT, padx=5\n",
    "        )\n",
    "\n",
    "        # Solve remotely (on server)\n",
    "        ttk.Button(solve_frame, text=\"Server mode solve\", command=self._solve_server_mode).pack(\n",
    "            side=tk.LEFT, padx=5\n",
    "        )\n",
    "\n",
    "        # Variables to store mode data and waveguide object\n",
    "        self.mode_data = None\n",
    "        self.current_waveguide = None\n",
    "        self.current_mode_index = 0\n",
    "\n",
    "        # Initialize the interface with default waveguide type\n",
    "        self._on_type_change()\n",
    "\n",
    "    def _on_type_change(self):\n",
    "        \"\"\"Handle changes in the waveguide type selection.\"\"\"\n",
    "        waveguide_type = self.waveguide_type_var.get()\n",
    "\n",
    "        # Hide all parameter frames\n",
    "        self.strip_frame.pack_forget()\n",
    "        self.rib_frame.pack_forget()\n",
    "        self.slot_frame.pack_forget()\n",
    "\n",
    "        # Show the parameter frame corresponding to the selected type\n",
    "        if waveguide_type == \"Strip waveguide\":\n",
    "            self.strip_frame.pack(after=self.type_frame, fill=tk.X, padx=5, pady=5)\n",
    "        elif waveguide_type == \"Rib waveguide\":\n",
    "            self.rib_frame.pack(after=self.type_frame, fill=tk.X, padx=5, pady=5)\n",
    "        else:  # Slot waveguide\n",
    "            self.slot_frame.pack(after=self.type_frame, fill=tk.X, padx=5, pady=5)\n",
    "\n",
    "        # Update the plot whenever the waveguide type changes\n",
    "        self._update_plot()\n",
    "\n",
    "    def _create_waveguide(self):\n",
    "        \"\"\"Create the waveguide object based on current parameters.\"\"\"\n",
    "        try:\n",
    "            # Define materials from user-input indices\n",
    "            core = Medium(permittivity=self.core_index_var.get() ** 2)\n",
    "            clad = Medium(permittivity=self.clad_index_var.get() ** 2)\n",
    "            box = Medium(permittivity=self.box_index_var.get() ** 2)\n",
    "\n",
    "            # Common parameters\n",
    "            wavelength = self.wavelength_var.get()\n",
    "            grid_resolution = self.grid_resolution_var.get()\n",
    "            num_modes = self.num_modes_var.get()\n",
    "\n",
    "            # Optional parameters: Bend radius and target effective index\n",
    "            bend_radius = self._get_bend_radius()\n",
    "            target_neff = self._get_target_neff()\n",
    "\n",
    "            # Use PML or not\n",
    "            use_pml = self.use_pml_var.get() == \"True\"\n",
    "            num_pml = (12, 12) if use_pml else (0, 0)\n",
    "\n",
    "            # Create ModeSpec object with user-defined parameters\n",
    "            mode_spec_params = {\n",
    "                \"num_modes\": num_modes,\n",
    "                \"bend_radius\": bend_radius,\n",
    "                \"num_pml\": num_pml,\n",
    "                \"group_index_step\": True,\n",
    "                \"precision\": \"double\",\n",
    "            }\n",
    "\n",
    "            if target_neff is not None:\n",
    "                mode_spec_params[\"target_neff\"] = target_neff\n",
    "\n",
    "            if bend_radius is not None:\n",
    "                # If bend_radius is given, bend axis is set to 1 (for curved waveguides)\n",
    "                mode_spec_params[\"bend_axis\"] = 1\n",
    "\n",
    "            mode_spec = ModeSpec(**mode_spec_params)\n",
    "\n",
    "            # Retrieve parameters based on selected waveguide type\n",
    "            waveguide_type = self.waveguide_type_var.get()\n",
    "\n",
    "            if waveguide_type == \"Strip waveguide\":\n",
    "                width = self.core_width_var.get()\n",
    "                thickness = self.core_thickness_var.get()\n",
    "                sidewall_angle_rad = math.radians(self.sidewall_angle_var.get())\n",
    "                slab_thickness = 0.0\n",
    "                gap = 0.0\n",
    "            elif waveguide_type == \"Rib waveguide\":\n",
    "                width = self.rib_width_var.get()\n",
    "                thickness = self.rib_thickness_var.get()\n",
    "                sidewall_angle_rad = math.radians(self.rib_angle_var.get())\n",
    "                slab_thickness = self.slab_thickness_var.get()\n",
    "                gap = 0.0\n",
    "            else:  # Slot waveguide\n",
    "                # Slot waveguide may have two core widths and a gap\n",
    "                width = [self.first_core_width_var.get(), self.second_core_width_var.get()]\n",
    "                thickness = self.slot_thickness_var.get()\n",
    "                sidewall_angle_rad = math.radians(self.slot_angle_var.get())\n",
    "                slab_thickness = 0.0\n",
    "                gap = self.gap_var.get()\n",
    "\n",
    "            # Create the RectangularDielectric waveguide object\n",
    "            waveguide = RectangularDielectric(\n",
    "                core_width=width,\n",
    "                core_thickness=thickness,\n",
    "                wavelength=wavelength,\n",
    "                core_medium=core,\n",
    "                clad_medium=clad,\n",
    "                box_medium=box,\n",
    "                clad_thickness=self.clad_thickness_var.get(),\n",
    "                box_thickness=self.box_thickness_var.get(),\n",
    "                slab_thickness=slab_thickness,\n",
    "                sidewall_angle=sidewall_angle_rad,\n",
    "                gap=gap,\n",
    "                mode_spec=mode_spec,\n",
    "                grid_resolution=grid_resolution,\n",
    "            )\n",
    "\n",
    "            return waveguide\n",
    "\n",
    "        except ValueError:\n",
    "            # If user inputs invalid values, show an error message\n",
    "            messagebox.showerror(\"Input Error\", \"Please enter valid numbers for all fields.\")\n",
    "            return None\n",
    "        except Exception as e:\n",
    "            # Catch any other exceptions\n",
    "            messagebox.showerror(\"Error\", str(e))\n",
    "            return None\n",
    "\n",
    "    def _update_plot(self):\n",
    "        \"\"\"Redraw the waveguide cross-section plot based on current parameters.\"\"\"\n",
    "        waveguide = self._create_waveguide()\n",
    "        if waveguide is None:\n",
    "            return\n",
    "\n",
    "        try:\n",
    "            # Clear the previous plot\n",
    "            self.ax.clear()\n",
    "\n",
    "            # Plot the waveguide cross-section\n",
    "            waveguide.mode_solver.plot(ax=self.ax)\n",
    "            self.ax.set_title(\"Mode solver cross-section\")\n",
    "\n",
    "            # Update the canvas to show the new plot\n",
    "            self.canvas.draw()\n",
    "\n",
    "        except Exception as e:\n",
    "            # If plotting fails, show an error\n",
    "            messagebox.showerror(\"Plot Error\", str(e))\n",
    "\n",
    "    def _create_mode_window(self, mode_index, mode_data):\n",
    "        \"\"\"\n",
    "        Create a separate window that displays the properties and field profile\n",
    "        of a single mode solution.\n",
    "        \"\"\"\n",
    "        # Create a new top-level window\n",
    "        mode_window = tk.Toplevel(self.root)\n",
    "        mode_window.title(f\"Mode {mode_index}\")\n",
    "\n",
    "        # Frame to hold mode properties (n_eff, group index, polarization fractions, etc.)\n",
    "        props_frame = ttk.Frame(mode_window)\n",
    "        props_frame.pack(pady=5, padx=10, fill=tk.X)\n",
    "\n",
    "        # Extract mode properties from mode_data\n",
    "        n_eff = float(mode_data.n_eff.values[0][mode_index])\n",
    "        k_eff = float(mode_data.k_eff.values[0][mode_index])\n",
    "        n_group = float(mode_data.n_group.values[0][mode_index])\n",
    "        te_frac = float(mode_data.pol_fraction.te.values[0][mode_index])\n",
    "        tm_frac = float(mode_data.pol_fraction.tm.values[0][mode_index])\n",
    "        mode_area = float(mode_data.mode_area.values[0][mode_index])\n",
    "\n",
    "        # Create labels to display these properties\n",
    "        props = [\n",
    "            (\"n_eff\", f\"{n_eff:.6f}\"),\n",
    "            (\"k_eff\", f\"{k_eff:.6f}\"),\n",
    "            (\"Group Index\", f\"{n_group:.6f}\"),\n",
    "            (\"TE Fraction\", f\"{te_frac * 100:.1f}%\"),\n",
    "            (\"TM Fraction\", f\"{tm_frac * 100:.1f}%\"),\n",
    "            (\"Mode Area\", f\"{mode_area:.2f} um²\"),\n",
    "        ]\n",
    "\n",
    "        # Display the properties in a grid layout\n",
    "        for i, (label, value) in enumerate(props):\n",
    "            ttk.Label(props_frame, text=f\"{label}:\").grid(\n",
    "                row=i, column=0, sticky=\"e\", padx=5, pady=2\n",
    "            )\n",
    "            ttk.Label(props_frame, text=value).grid(row=i, column=1, sticky=\"w\", padx=5, pady=2)\n",
    "\n",
    "        # Create a Matplotlib plot for the mode field\n",
    "        fig, ax = plt.subplots(figsize=(6, 4))\n",
    "        canvas = FigureCanvasTkAgg(fig, master=mode_window)\n",
    "        canvas.get_tk_widget().pack(pady=5)\n",
    "\n",
    "        # Plot the electric field (absolute value) of this mode\n",
    "        self.current_waveguide.plot_field(field_name=\"E\", val=\"abs\", mode_index=mode_index, ax=ax)\n",
    "        ax.set_title(\"Mode profile\")\n",
    "        canvas.draw()\n",
    "\n",
    "    def _solve_local_mode(self):\n",
    "        \"\"\"Solve for modes locally and display results.\"\"\"\n",
    "        try:\n",
    "            self.current_waveguide = self._create_waveguide()\n",
    "            if self.current_waveguide is None:\n",
    "                return\n",
    "\n",
    "            # Solve the mode problem locally\n",
    "            self.mode_data = self.current_waveguide.mode_solver.solve()\n",
    "\n",
    "            # Create a separate window for each mode to display its properties and fields\n",
    "            for mode_index in range(len(self.mode_data.n_eff.values[0])):\n",
    "                self._create_mode_window(mode_index, self.mode_data)\n",
    "\n",
    "        except Exception as e:\n",
    "            # Print error details for debugging\n",
    "            print(\"Error in local mode solve:\", str(e))\n",
    "            print(\"Full error:\", traceback.format_exc())\n",
    "            messagebox.showerror(\"Error\", str(e))\n",
    "\n",
    "    def _solve_server_mode(self):\n",
    "        \"\"\"Solve for modes on a remote server and display results.\"\"\"\n",
    "        try:\n",
    "            self.current_waveguide = self._create_waveguide()\n",
    "            if self.current_waveguide is None:\n",
    "                return\n",
    "\n",
    "            # Create a small progress window while solving on the server\n",
    "            progress_window = tk.Toplevel(self.root)\n",
    "            progress_window.title(\"Server Mode Solve\")\n",
    "            progress_window.geometry(\"300x80\")\n",
    "            progress_window.transient(self.root)\n",
    "            progress_window.grab_set()  # Make it modal\n",
    "\n",
    "            # Center the progress window on the screen\n",
    "            window_width = 300\n",
    "            window_height = 80\n",
    "            screen_width = self.root.winfo_screenwidth()\n",
    "            screen_height = self.root.winfo_screenheight()\n",
    "            x = (screen_width - window_width) // 2\n",
    "            y = (screen_height - window_height) // 2\n",
    "            progress_window.geometry(f\"{window_width}x{window_height}+{x}+{y}\")\n",
    "\n",
    "            # Label inside the progress window\n",
    "            message = tk.Label(\n",
    "                progress_window, text=\"Solving modes on server...\\nThis may take a few moments.\"\n",
    "            )\n",
    "            message.pack(expand=True)\n",
    "\n",
    "            try:\n",
    "                # Update the GUI so the message is shown\n",
    "                progress_window.update()\n",
    "\n",
    "                # Run the mode solver on the server\n",
    "                self.mode_data = run_mode_solver(self.current_waveguide.mode_solver)\n",
    "\n",
    "                # Close the progress window after completion\n",
    "                progress_window.destroy()\n",
    "\n",
    "                # Create a separate window for each mode result\n",
    "                for mode_index in range(len(self.mode_data.n_eff.values[0])):\n",
    "                    self._create_mode_window(mode_index, self.mode_data)\n",
    "\n",
    "            except Exception as server_error:\n",
    "                # If there's an error during server solve, close the progress window and show an error\n",
    "                progress_window.destroy()\n",
    "                print(\"Error during server mode solve:\", str(server_error))\n",
    "                print(\"Full server error:\", traceback.format_exc())\n",
    "                messagebox.showerror(\n",
    "                    \"Server Error\", f\"Error during server mode solve: {str(server_error)}\"\n",
    "                )\n",
    "                return\n",
    "\n",
    "        except Exception as e:\n",
    "            # Catch any other errors\n",
    "            print(\"Error in server mode solve:\", str(e))\n",
    "            print(\"Full error:\", traceback.format_exc())\n",
    "            messagebox.showerror(\"Error\", str(e))\n",
    "\n",
    "    def _reset_values(self):\n",
    "        \"\"\"Reset all parameters to default values.\"\"\"\n",
    "        # Reset waveguide type and parameters\n",
    "        self.waveguide_type_var.set(\"Strip waveguide\")\n",
    "        self._on_type_change()\n",
    "\n",
    "        # Strip parameters\n",
    "        self.core_width_var.set(0.5)\n",
    "        self.core_thickness_var.set(0.22)\n",
    "        self.sidewall_angle_var.set(10.0)\n",
    "\n",
    "        # Rib parameters\n",
    "        self.rib_width_var.set(0.5)\n",
    "        self.rib_thickness_var.set(0.22)\n",
    "        self.rib_angle_var.set(10.0)\n",
    "        self.slab_thickness_var.set(0.1)\n",
    "\n",
    "        # Slot parameters\n",
    "        self.first_core_width_var.set(0.5)\n",
    "        self.second_core_width_var.set(0.5)\n",
    "        self.gap_var.set(0.1)\n",
    "        self.slot_thickness_var.set(0.22)\n",
    "        self.slot_angle_var.set(10.0)\n",
    "\n",
    "        # Common parameters\n",
    "        self.core_index_var.set(3.47)\n",
    "        self.clad_index_var.set(1.0)\n",
    "        self.box_index_var.set(1.44)\n",
    "        self.clad_thickness_var.set(2.0)\n",
    "        self.box_thickness_var.set(2.0)\n",
    "\n",
    "        # Simulation parameters\n",
    "        self.wavelength_var.set(1.55)\n",
    "        self.grid_resolution_var.set(25)\n",
    "        self.num_modes_var.set(1)\n",
    "\n",
    "        # Update the plot after resetting\n",
    "        self._update_plot()\n",
    "\n",
    "    def _validate_float_or_empty(self, value):\n",
    "        \"\"\"Check if the input is empty or a valid float.\"\"\"\n",
    "        if value == \"\":\n",
    "            return True\n",
    "        try:\n",
    "            float(value)\n",
    "            return True\n",
    "        except ValueError:\n",
    "            return False\n",
    "\n",
    "    def _get_bend_radius(self):\n",
    "        \"\"\"Retrieve the bend radius value; return None if empty or invalid.\"\"\"\n",
    "        value = self.bend_radius_entry.get().strip()\n",
    "        if not value:\n",
    "            return None\n",
    "        try:\n",
    "            return float(value)\n",
    "        except ValueError:\n",
    "            return None\n",
    "\n",
    "    def _get_target_neff(self):\n",
    "        \"\"\"Retrieve the target n_eff value; return None if empty or invalid.\"\"\"\n",
    "        value = self.target_neff_entry.get().strip()\n",
    "        if not value:\n",
    "            return None\n",
    "        try:\n",
    "            return float(value)\n",
    "        except ValueError:\n",
    "            return None\n",
    "\n",
    "\n",
    "if __name__ == \"__main__\":\n",
    "    # Instantiate and run the Tkinter application\n",
    "    root = tk.Tk()\n",
    "    app = WaveguideGUI(root)\n",
    "    root.mainloop()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "de1409f1-a714-465b-9dcb-bf6328918b2c",
   "metadata": {},
   "source": [
    "## Final Remarks\n",
    "\n",
    "This mode solver GUI app showcases the flexibility of creating tailored applications that can be precisely adapted to your unique workflow. This versatile tool provides a foundation that can be easily extended to incorporate additional features and enhanced functionalities. For another illustration, we invite you to explore our specialized GUI designed for [waveguide bend simulations](https://www.flexcompute.com/tidy3d/examples/notebooks/WaveguideBendSimulator/), which also demonstrates the potential for customized interfaces."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6dbe283e-178d-4595-8631-1b2d1a9140b4",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "description": "This notebook demonstrates how to create a mode solver GUI using Tidy3D and tkinter.",
  "feature_image": "./img/mode_solver_gui.png",
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "keywords": "mode solver, waveguide, mode, GUI, Tidy3D, FDTD",
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.0"
  },
  "title": "Build a simple waveguide mode solver GUI in Tidy3D | Flexcompute"
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
