Source code for complex_problems.antenna_radiation.ui

"""UI dialog for configuring antenna radiation simulations."""

from __future__ import annotations

import tkinter as tk
from tkinter import messagebox, ttk

from complex_problems.antenna_radiation.solver import solve_antenna_radiation
from complex_problems.common import (
    add_how_to_config_section,
    parse_float,
    parse_positive_float,
    parse_positive_int,
    run_solver_with_loading,
)
from config import get_env_from_schema
from frontend.theme import get_font
from frontend.ui_dialogs.scrollable_frame import ScrollableFrame
from frontend.ui_dialogs.tooltip import ToolTip
from frontend.window_utils import fit_and_center, make_modal

_ANTENNA_TYPES = ("dipole", "loop", "patch", "array")


[docs] class AntennaRadiationDialog: """Configuration dialog for antenna radiation patterns.""" def __init__(self, parent: tk.Tk | tk.Toplevel) -> None: self.parent = parent self.win = tk.Toplevel(parent) self.win.title("Antenna Radiation") self.win.configure(bg=get_env_from_schema("UI_BACKGROUND")) self._build_ui() fit_and_center(self.win, min_width=980, min_height=760, padding=32, resizable=True) self.win.minsize(920, 700) make_modal(self.win, parent) def _build_ui(self) -> None: pad = int(get_env_from_schema("UI_PADDING")) root = ttk.Frame(self.win, padding=pad * 2) root.pack(fill=tk.BOTH, expand=True) scroll = ScrollableFrame(root) scroll.apply_bg(get_env_from_schema("UI_BACKGROUND")) scroll.pack(fill=tk.BOTH, expand=True) body = scroll.inner body.configure(padding=pad) ttk.Label(body, text="Antenna Radiation", style="Title.TLabel").pack(anchor=tk.W) ttk.Label( body, text=( "Far-field radiation maps, gain/directivity, and field magnitudes.\n" "Choose the antenna family and set its geometric parameters." ), style="Small.TLabel", justify=tk.LEFT, ).pack(anchor=tk.W, pady=(0, pad)) add_how_to_config_section( body, scroll, problem_id="antenna_radiation", pad=pad, wraplength=780, ) row = ttk.Frame(body) row.pack(fill=tk.X, pady=pad // 2) self._antenna_type_var = tk.StringVar(value="dipole") at_combo = self._make_combo( row, "Antenna type", self._antenna_type_var, _ANTENNA_TYPES, width=12, ) at_combo.bind("<<ComboboxSelected>>", lambda _e: self._update_visibility()) row = ttk.Frame(body) row.pack(fill=tk.X, pady=pad // 2) self._frequency_mhz_var = tk.StringVar(value="1000") self._power_w_var = tk.StringVar(value="10") self._efficiency_var = tk.StringVar(value="0.90") self._distance_m_var = tk.StringVar(value="50") self._make_entry(row, "Frequency (MHz)", self._frequency_mhz_var, width=11) self._make_entry(row, "Pₜₓ (W)", self._power_w_var, width=9) self._make_entry(row, "Efficiency η", self._efficiency_var, width=10) self._make_entry(row, "Observation r (m)", self._distance_m_var, width=13) ToolTip(row, "Efficiency must be between 0 and 1.") row = ttk.Frame(body) row.pack(fill=tk.X, pady=pad // 2) self._n_theta_var = tk.StringVar(value="181") self._n_phi_var = tk.StringVar(value="360") self._make_entry(row, "N_θ", self._n_theta_var, width=8) self._make_entry(row, "N_φ", self._n_phi_var, width=8) ttk.Separator(body).pack(fill=tk.X, pady=pad) ttk.Label(body, text="Antenna parameters", style="Small.TLabel").pack(anchor=tk.W) self._dipole_row = ttk.Frame(body) self._dipole_row.pack(fill=tk.X, pady=pad // 2) self._dipole_length_var = tk.StringVar(value="0.5") self._make_entry(self._dipole_row, "Length (λ)", self._dipole_length_var, width=10) self._loop_row = ttk.Frame(body) self._loop_row.pack(fill=tk.X, pady=pad // 2) self._loop_radius_var = tk.StringVar(value="0.10") self._make_entry(self._loop_row, "Radius (λ)", self._loop_radius_var, width=10) self._patch_row = ttk.Frame(body) self._patch_row.pack(fill=tk.X, pady=pad // 2) self._patch_length_var = tk.StringVar(value="0.5") self._patch_width_var = tk.StringVar(value="0.4") self._make_entry(self._patch_row, "Patch L (λ)", self._patch_length_var, width=10) self._make_entry(self._patch_row, "Patch W (λ)", self._patch_width_var, width=10) self._array_row = ttk.Frame(body) self._array_row.pack(fill=tk.X, pady=pad // 2) self._array_elements_var = tk.StringVar(value="8") self._array_spacing_var = tk.StringVar(value="0.5") self._array_phase_var = tk.StringVar(value="0.0") self._array_steer_var = tk.StringVar(value="90.0") self._make_entry(self._array_row, "Elements", self._array_elements_var, width=8) self._make_entry(self._array_row, "Spacing (λ)", self._array_spacing_var, width=10) self._make_entry(self._array_row, "Phase (deg)", self._array_phase_var, width=10) self._make_entry(self._array_row, "Steer θ (deg)", self._array_steer_var, width=12) self._btn_row = ttk.Frame(body) self._btn_row.pack(fill=tk.X, pady=(pad * 2, 0)) ttk.Button(self._btn_row, text="Solve", command=self._on_solve).pack( side=tk.LEFT, padx=(0, pad) ) ttk.Button( self._btn_row, text="Close", style="Cancel.TButton", command=self.win.destroy, ).pack( side=tk.LEFT ) self._update_visibility() scroll.bind_new_children() def _make_entry( self, parent: ttk.Frame, label: str, var: tk.StringVar, *, width: int = 10 ) -> ttk.Entry: ttk.Label(parent, text=f"{label}:").pack(side=tk.LEFT, padx=(0, 4)) entry = ttk.Entry(parent, textvariable=var, width=width, font=get_font()) entry.pack(side=tk.LEFT, padx=(0, 12)) return entry def _make_combo( self, parent: ttk.Frame, label: str, var: tk.StringVar, values: tuple[str, ...], *, width: int = 12, ) -> ttk.Combobox: ttk.Label(parent, text=f"{label}:").pack(side=tk.LEFT, padx=(0, 4)) combo = ttk.Combobox( parent, textvariable=var, values=list(values), state="readonly", width=width, font=get_font(), ) combo.pack(side=tk.LEFT, padx=(0, 12)) return combo def _update_visibility(self) -> None: atype = self._antenna_type_var.get() self._dipole_row.pack_forget() self._loop_row.pack_forget() self._patch_row.pack_forget() self._array_row.pack_forget() if atype == "dipole": self._dipole_row.pack(fill=tk.X, pady=4, before=self._btn_row) elif atype == "loop": self._loop_row.pack(fill=tk.X, pady=4, before=self._btn_row) elif atype == "patch": self._patch_row.pack(fill=tk.X, pady=4, before=self._btn_row) else: self._array_row.pack(fill=tk.X, pady=4, before=self._btn_row) def _collect_inputs(self) -> dict[str, object]: frequency_mhz = parse_positive_float(self._frequency_mhz_var.get(), name="Frequency (MHz)") frequency_hz = frequency_mhz * 1.0e6 power_w = parse_positive_float(self._power_w_var.get(), name="Pₜₓ") efficiency = parse_float(self._efficiency_var.get(), name="Efficiency η") if efficiency <= 0 or efficiency > 1: raise ValueError("Efficiency η must be in (0, 1].") distance_m = parse_positive_float(self._distance_m_var.get(), name="Observation r") n_theta = parse_positive_int(self._n_theta_var.get(), name="N_θ", min_value=21) n_phi = parse_positive_int(self._n_phi_var.get(), name="N_φ", min_value=32) length_lambda = parse_positive_float(self._dipole_length_var.get(), name="Length (λ)") loop_radius_lambda = parse_positive_float( self._loop_radius_var.get(), name="Radius (λ)", ) patch_length_lambda = parse_positive_float(self._patch_length_var.get(), name="Patch L") patch_width_lambda = parse_positive_float(self._patch_width_var.get(), name="Patch W") array_elements = parse_positive_int( self._array_elements_var.get(), name="Elements", min_value=2, ) array_spacing_lambda = parse_positive_float( self._array_spacing_var.get(), name="Spacing (λ)", ) array_phase_deg = parse_float(self._array_phase_var.get(), name="Phase") array_steer_deg = parse_float(self._array_steer_var.get(), name="Steer θ") return { "antenna_type": self._antenna_type_var.get(), "frequency_hz": frequency_hz, "transmit_power_w": power_w, "efficiency": efficiency, "observation_distance_m": distance_m, "n_theta": n_theta, "n_phi": n_phi, "length_lambda": length_lambda, "loop_radius_lambda": loop_radius_lambda, "patch_length_lambda": patch_length_lambda, "patch_width_lambda": patch_width_lambda, "array_elements": array_elements, "array_spacing_lambda": array_spacing_lambda, "array_phase_deg": array_phase_deg, "array_steer_theta_deg": array_steer_deg, } def _on_solve(self) -> None: try: params = self._collect_inputs() except ValueError as exc: messagebox.showerror("Invalid input", str(exc), parent=self.win) return self.win.destroy() def _task(): return solve_antenna_radiation(**params) def _on_success(result) -> None: from complex_problems.antenna_radiation.result_dialog import ( AntennaRadiationResultDialog, ) AntennaRadiationResultDialog(self.parent, result=result) run_solver_with_loading( parent=self.parent, message="Solving antenna radiation...", task=_task, on_success=_on_success, )