Source code for complex_problems.nonlinear_waves.ui

"""UI dialog for nonlinear wave simulations (NLSE / KdV)."""

from __future__ import annotations

import tkinter as tk
from tkinter import messagebox, ttk

from complex_problems.common import (
    add_how_to_config_section,
    compile_scalar_expression,
    parse_float,
    parse_positive_float,
    parse_positive_int,
    run_solver_with_loading,
)
from complex_problems.nonlinear_waves.solver import solve_nonlinear_waves
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

_MODELS = ("nlse", "kdv")
_PROFILES = ("sech", "gaussian", "pulse", "custom")


[docs] class NonlinearWavesDialog: """Configuration dialog for nonlinear wave propagation models.""" def __init__(self, parent: tk.Tk | tk.Toplevel) -> None: self.parent = parent self.win = tk.Toplevel(parent) self.win.title("Nonlinear Waves (NLSE + KdV)") 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="Nonlinear Waves", style="Title.TLabel").pack(anchor=tk.W) ttk.Label( body, text=( "Choose NLSE (complex envelope) or KdV (real nonlinear dispersive wave).\n" "Both use periodic pseudo-spectral solvers." ), style="Small.TLabel", justify=tk.LEFT, ).pack(anchor=tk.W, pady=(0, pad)) add_how_to_config_section( body, scroll, problem_id="nonlinear_waves", pad=pad, wraplength=780, ) row = ttk.Frame(body) row.pack(fill=tk.X, pady=pad // 2) self._model_var = tk.StringVar(value="nlse") model_combo = self._make_combo(row, "Model", self._model_var, _MODELS, width=10) model_combo.bind("<<ComboboxSelected>>", lambda _e: self._update_model_visibility()) ToolTip(model_combo, "NLSE: split-step Fourier. KdV: pseudo-spectral ETDRK4.") row = ttk.Frame(body) row.pack(fill=tk.X, pady=pad // 2) self._x_min_var = tk.StringVar(value="-20.0") self._x_max_var = tk.StringVar(value="20.0") self._nx_var = tk.StringVar(value="512") self._make_entry(row, "xₘᵢₙ", self._x_min_var, width=10) self._make_entry(row, "xₘₐₓ", self._x_max_var, width=10) self._make_spinbox(row, "Nₓ", self._nx_var, from_=64, to=32768, width=9) row = ttk.Frame(body) row.pack(fill=tk.X, pady=pad // 2) self._t_min_var = tk.StringVar(value="0.0") self._t_max_var = tk.StringVar(value="8.0") self._dt_var = tk.StringVar(value="0.002") self._make_entry(row, "tₘᵢₙ", self._t_min_var, width=10) self._make_entry(row, "tₘₐₓ", self._t_max_var, width=10) self._make_entry(row, "Δt", self._dt_var, width=10) ttk.Separator(body).pack(fill=tk.X, pady=pad) ttk.Label(body, text="Initial profile", style="Small.TLabel").pack(anchor=tk.W) self._profile_row = ttk.Frame(body) self._profile_row.pack(fill=tk.X, pady=pad // 2) self._profile_var = tk.StringVar(value="sech") profile_combo = self._make_combo( self._profile_row, "Profile", self._profile_var, _PROFILES, width=12 ) profile_combo.bind("<<ComboboxSelected>>", lambda _e: self._update_profile_visibility()) self._profile_params_row = ttk.Frame(body) self._profile_params_row.pack(fill=tk.X, pady=pad // 2) self._amp_var = tk.StringVar(value="1.0") self._sigma_var = tk.StringVar(value="1.0") self._center_var = tk.StringVar(value="0.0") self._make_entry(self._profile_params_row, "Amplitude", self._amp_var, width=8) self._make_entry(self._profile_params_row, "σ", self._sigma_var, width=8) self._make_entry(self._profile_params_row, "Center x₀", self._center_var, width=9) self._custom_row = ttk.Frame(body) self._custom_row.pack(fill=tk.X, pady=pad // 2) ttk.Label(self._custom_row, text="u₀(x) =").pack(side=tk.LEFT, padx=(0, 4)) self._custom_expr_var = tk.StringVar(value="exp(-x**2)") self._custom_entry = ttk.Entry( self._custom_row, textvariable=self._custom_expr_var, width=52, font=get_font(), ) self._custom_entry.pack(side=tk.LEFT, fill=tk.X, expand=True) ToolTip(self._custom_entry, "Custom expression in x.") ttk.Separator(body).pack(fill=tk.X, pady=pad) ttk.Label(body, text="Model parameters", style="Small.TLabel").pack(anchor=tk.W) self._nlse_row = ttk.Frame(body) self._nlse_row.pack(fill=tk.X, pady=pad // 2) self._beta2_var = tk.StringVar(value="1.0") self._gamma_var = tk.StringVar(value="1.0") self._phase_k_var = tk.StringVar(value="0.0") self._make_entry(self._nlse_row, "β₂", self._beta2_var, width=8) self._make_entry(self._nlse_row, "γ", self._gamma_var, width=8) self._make_entry(self._nlse_row, "Phase k₀", self._phase_k_var, width=9) self._kdv_row = ttk.Frame(body) self._kdv_row.pack(fill=tk.X, pady=pad // 2) self._c_var = tk.StringVar(value="0.0") self._alpha_var = tk.StringVar(value="6.0") self._beta_disp_var = tk.StringVar(value="1.0") self._make_entry(self._kdv_row, "c", self._c_var, width=8) self._make_entry(self._kdv_row, "α", self._alpha_var, width=8) self._make_entry(self._kdv_row, "β", self._beta_disp_var, width=8) 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_profile_visibility() self._update_model_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_spinbox( self, parent: ttk.Frame, label: str, var: tk.StringVar, *, from_: int, to: int, width: int = 10, ) -> ttk.Spinbox: ttk.Label(parent, text=f"{label}:").pack(side=tk.LEFT, padx=(0, 4)) spin = ttk.Spinbox( parent, textvariable=var, from_=from_, to=to, width=width, font=get_font(), ) spin.pack(side=tk.LEFT, padx=(0, 12)) return spin 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_profile_visibility(self) -> None: if self._profile_var.get() == "custom": self._custom_row.pack(fill=tk.X, pady=4, before=self._btn_row) else: self._custom_row.pack_forget() def _update_model_visibility(self) -> None: if self._model_var.get() == "nlse": self._nlse_row.pack(fill=tk.X, pady=4, before=self._btn_row) self._kdv_row.pack_forget() else: self._kdv_row.pack(fill=tk.X, pady=4, before=self._btn_row) self._nlse_row.pack_forget() def _collect_inputs(self) -> dict[str, object]: model = self._model_var.get() x_min = parse_float(self._x_min_var.get(), name="xₘᵢₙ") x_max = parse_float(self._x_max_var.get(), name="xₘₐₓ") nx = parse_positive_int(self._nx_var.get(), name="Nₓ", min_value=64) t_min = parse_float(self._t_min_var.get(), name="tₘᵢₙ") t_max = parse_float(self._t_max_var.get(), name="tₘₐₓ") dt = parse_positive_float(self._dt_var.get(), name="Δt") profile = self._profile_var.get() amplitude = parse_float(self._amp_var.get(), name="Amplitude") sigma = parse_positive_float(self._sigma_var.get(), name="σ") center = parse_float(self._center_var.get(), name="Center x₀") custom_fn = None if profile == "custom": custom_fn = compile_scalar_expression( self._custom_expr_var.get(), variables=("x",), ) params: dict[str, object] = { "model_type": model, "x_min": x_min, "x_max": x_max, "nx": nx, "t_min": t_min, "t_max": t_max, "dt": dt, "profile": profile, "amplitude": amplitude, "sigma": sigma, "center": center, "custom_profile_fn": custom_fn, } if model == "nlse": params["beta2"] = parse_float(self._beta2_var.get(), name="β₂") params["gamma"] = parse_float(self._gamma_var.get(), name="γ") params["initial_phase_k"] = parse_float(self._phase_k_var.get(), name="Phase k₀") else: params["c"] = parse_float(self._c_var.get(), name="c") params["alpha"] = parse_float(self._alpha_var.get(), name="α") params["beta_disp"] = parse_float(self._beta_disp_var.get(), name="β") return params 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_nonlinear_waves(**params) def _on_success(result) -> None: from complex_problems.nonlinear_waves.result_dialog import NonlinearWavesResultDialog NonlinearWavesResultDialog(self.parent, result=result) run_solver_with_loading( parent=self.parent, message="Solving nonlinear wave propagation...", task=_task, on_success=_on_success, )