From 3fbd8da66f951b7d584ba422f338e7b6b55db3b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Beno=C3=AEt=20Sierro?= Date: Mon, 10 Jul 2023 16:03:41 +0200 Subject: [PATCH] added refractive index plot --- .gitignore | 1 + README.md | 2 +- pyproject.toml | 4 +- src/dispersionapp/core.py | 16 ++++-- src/dispersionapp/gui.py | 93 +++++++++++++++++++++--------- src/dispersionapp/plotapp.py | 106 ++++++----------------------------- 6 files changed, 98 insertions(+), 124 deletions(-) diff --git a/.gitignore b/.gitignore index 3b8bc6f..3c69754 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.conda-env dispersion_config.toml pyrightconfig.json .DS_Store diff --git a/README.md b/README.md index 36fc097..4c71de7 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ conda activate app-env 3. The prompt should now read '(app-env)' on the left. The app is not published on Github or anywhere else. The link below points to my own personnal home server (I didn't find any way of getting a direct download link You are now ready to install everything with this command: -pip install "git+file://" +pip install http://130.92.113.172/dispersionapp_v0.1.0.zip # Usage diff --git a/pyproject.toml b/pyproject.toml index 7e2f23e..3c3dd94 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,13 +1,13 @@ [project] name = "dispersionapp" -version = "0.1.0" +version = "0.1.3" description = "Model hollow capillary and revolver fiber interactively" authors = [{ name = "Benoît Sierro", email = "benoit.sierro@unibe.ch" }] dependencies = [ "scgenerator @ git+https://github.com/bsierro/scgenerator.git", "click", - "pydantic", + "pydantic < 2", "tomli", "tomli_w", "PySide6 >= 6.4.0", diff --git a/src/dispersionapp/core.py b/src/dispersionapp/core.py index f597ae6..28fb50d 100644 --- a/src/dispersionapp/core.py +++ b/src/dispersionapp/core.py @@ -2,7 +2,7 @@ from __future__ import annotations import sys from pathlib import Path -from typing import NamedTuple +from typing import NamedTuple import numpy as np import scgenerator as sc @@ -17,6 +17,7 @@ class CurrentState(BaseModel): core_diameter_um: float pressure_mbar: float wall_thickness_um: float + num_resonances: conint(ge=6, le=40) = 6 n_tubes: int gap_um: float t_fwhm_fs: float @@ -27,7 +28,6 @@ class Config(BaseModel): wl_max: confloat(ge=500, le=6000) = 1600 wl_pump: confloat(ge=200, le=6000) = 800 rep_rate: confloat(gt=0) = 8e3 - num_resonances: conint(ge=6, le=20) = 6 gas: str = "argon" safety_factor: float = 10.0 current_state: CurrentState | None = None @@ -45,7 +45,7 @@ class Config(BaseModel): try: out = cls(**d) except ValidationError as e: - s = f"invalid input in config file {config_file}:\n{e}" + s = f"invalid input in config file {config_file}:\n{e}\ncreating new config" print(s) sys.exit(1) out._file_name = Path(config_file) @@ -58,12 +58,20 @@ class Config(BaseModel): tmp.replace(self._file_name) def update_current( - self, core_diameter_um, pressure_mbar, wall_thickness_um, n_tubes, gap_um, t_fwhm_fs + self, + core_diameter_um: float, + pressure_mbar: float, + wall_thickness_um: float, + num_resonances: int, + n_tubes: int, + gap_um: float, + t_fwhm_fs: float, ): self.current_state = CurrentState( core_diameter_um=core_diameter_um, pressure_mbar=pressure_mbar, wall_thickness_um=wall_thickness_um, + num_resonances=num_resonances, n_tubes=n_tubes, gap_um=gap_um, t_fwhm_fs=t_fwhm_fs, diff --git a/src/dispersionapp/gui.py b/src/dispersionapp/gui.py index d26b9e5..6338ff3 100644 --- a/src/dispersionapp/gui.py +++ b/src/dispersionapp/gui.py @@ -7,8 +7,9 @@ from functools import cache import numpy as np import scgenerator as sc -from dispersionapp.core import Config, LimitValues, N_ion_max, N_sf_max, b2, energy -from dispersionapp.plotapp import PlotApp +from dispersionapp.core import (Config, LimitValues, N_ion_max, N_sf_max, b2, + energy) +from dispersionapp.plotapp import PlotApp, QtWidgets def app(config_file: os.PathLike | None = None): @@ -19,27 +20,34 @@ def app(config_file: os.PathLike | None = None): wl_ind = sc.math.argclosest(wl, config.wl_pump * 1e-9) w0 = w[wl_ind] - gas = sc.materials.Gas(config.gas) with PlotApp( f"Dispersion design with {config.gas.title()}", core_diameter_um=np.arange(50, 301, dtype=float), pressure_mbar=np.geomspace(1, 2000), - wall_thickness_um=np.geomspace(0.01, 10), + wall_thickness_um=np.geomspace(0.01, 10, 201), + num_resonances=np.arange(6, 41), n_tubes=np.arange(6, 16), gap_um=np.arange(1, 15.5, 0.5), t_fwhm_fs=np.arange(10, 201, dtype=float), ) as app: # initial setup - ax = app["Dispersion plot"] - ax.horizontal_line("reference", 0, color="gray") - ax.set_xlabel("wavelength (nm)") - ax.set_ylabel("beta2 (fs^2/cm)") + beta_ax = app["Dispersion plot"] + beta_ax.horizontal_line("reference", 0, color="gray") + beta_ax.set_xlabel("wavelength (nm)") + beta_ax.set_ylabel("beta2 (fs^2/cm)") + + n_ax = app["Refractive index (real)"] + n_ax.link_x(beta_ax) + n_ax.set_xlabel("wavelength (nm)") + n_ax.set_ylabel("n") + if config.current_state is not None: app.params["core_diameter_um"].value = config.current_state.core_diameter_um app.params["pressure_mbar"].value = config.current_state.pressure_mbar app.params["wall_thickness_um"].value = config.current_state.wall_thickness_um + app.params["num_resonances"].value = config.current_state.num_resonances app.params["n_tubes"].value = config.current_state.n_tubes app.params["gap_um"].value = config.current_state.gap_um app.params["t_fwhm_fs"].value = config.current_state.t_fwhm_fs @@ -47,10 +55,19 @@ def app(config_file: os.PathLike | None = None): app.params["core_diameter_um"].value = 100 app.params["pressure_mbar"].value = 500 app.params["wall_thickness_um"].value = 1 + app.params["num_resonances"].value = 6 app.params["n_tubes"].value = 7 app.params["gap_um"].value = 5 app.params["t_fwhm_fs"].value = 100 - ax.set_lim(ylim=(-4, 2)) + + def reset_view(): + # reseting beta_ax resets n_ax as well since they're linked + beta_ax.set_lim(ylim=(-4, 2)) + + reset_button = QtWidgets.QPushButton("Reset axes") + reset_button.clicked.connect(reset_view) + app.params_layout.addWidget(reset_button) + reset_view() app.update(config.update_current) @@ -86,13 +103,14 @@ def app(config_file: os.PathLike | None = None): ) @cache - def compute_vincetti( + def compute_n_eff_vincetti( wall_thickness_um: float, core_diameter_um: float, pressure_mbar: float, + num_resonances: int, n_tubes: int, gap_um: float, - ) -> np.ndarray: + ): core_diameter = core_diameter_um * 1e-6 wall_thickness = wall_thickness_um * 1e-6 gap = gap_um * 1e-6 @@ -100,15 +118,21 @@ def app(config_file: os.PathLike | None = None): tr = sc.fiber.tube_radius_from_gap(core_diameter / 2, gap, n_tubes) n_gas_2 = gas.sellmeier.n_gas_2(wl, None, pressure) - n_eff_vinc = sc.fiber.n_eff_vincetti( - wl, - 800e-9, - n_gas_2, - wall_thickness, - tr, - gap, - n_tubes, - n_terms=config.num_resonances, + return sc.fiber.n_eff_vincetti( + wl, 800e-9, n_gas_2, wall_thickness, tr, gap, n_tubes, n_terms=num_resonances + ) + + @cache + def compute_vincetti_beta( + wall_thickness_um: float, + core_diameter_um: float, + pressure_mbar: float, + num_resonances: int, + n_tubes: int, + gap_um: float, + ) -> np.ndarray: + n_eff_vinc = compute_n_eff_vincetti( + wall_thickness_um, core_diameter_um, pressure_mbar, num_resonances, n_tubes, gap_um ) return b2(w, n_eff_vinc) @@ -121,22 +145,37 @@ def app(config_file: os.PathLike | None = None): return b2(w, n_eff_cap) @app.update - def draw_vincetty( + def draw_vincetty_n_eff( wall_thickness_um: float, core_diameter_um: float, pressure_mbar: float, + num_resonances: int, n_tubes: int, gap_um: float, ): - b2 = compute_vincetti( - wall_thickness_um, core_diameter_um, pressure_mbar, n_tubes, gap_um + n_eff = compute_n_eff_vincetti( + wall_thickness_um, core_diameter_um, pressure_mbar, num_resonances, n_tubes, gap_um ) - ax.set_line_data("Vincetti", wl * 1e9, sc.units.beta2_fs_cm_inv(b2)) + n_ax.set_line_data("Vincetti", wl * 1e9, n_eff) + + @app.update + def draw_vincetty_beta( + wall_thickness_um: float, + core_diameter_um: float, + pressure_mbar: float, + num_resonances: int, + n_tubes: int, + gap_um: float, + ): + b2 = compute_vincetti_beta( + wall_thickness_um, core_diameter_um, pressure_mbar, num_resonances, n_tubes, gap_um + ) + beta_ax.set_line_data("Vincetti", wl * 1e9, sc.units.beta2_fs_cm_inv(b2)) @app.update def draw_capillary(core_diameter_um: float, pressure_mbar: float): b2 = compute_capillary(core_diameter_um, pressure_mbar) - ax.set_line_data("Capillary", wl * 1e9, sc.units.beta2_fs_cm_inv(b2)) + beta_ax.set_line_data("Capillary", wl * 1e9, sc.units.beta2_fs_cm_inv(b2)) @app.update def draw_energy_limit(core_diameter_um: float, pressure_mbar: float, t_fwhm_fs: float): @@ -153,7 +192,7 @@ def app(config_file: os.PathLike | None = None): return zdw = lim.wl_zero_disp * 1e9 - ax.set_line_data("zdw", [zdw, zdw], [-3, 3]) + beta_ax.set_line_data("zdw", [zdw, zdw], [-3, 3]) info_lines.append(f"ZDW = {zdw:.0f}nm") if lim.ion_lim > lim.sf_lim: @@ -170,6 +209,6 @@ def app(config_file: os.PathLike | None = None): f"N = {lim.soliton_sf_limit:.1f}", "limited by ionization", ] - app.info_label.setText("\n".join(info_lines)) + app.info_label.setText(" ".join(info_lines)) config.save() diff --git a/src/dispersionapp/plotapp.py b/src/dispersionapp/plotapp.py index 6b55672..ed161e5 100644 --- a/src/dispersionapp/plotapp.py +++ b/src/dispersionapp/plotapp.py @@ -5,7 +5,8 @@ import itertools from collections.abc import MutableMapping, Sequence from functools import cache from types import MethodType -from typing import Any, Callable, Iterable, Iterator, Optional, Type, Union, overload +from typing import (Any, Callable, Iterable, Iterator, Optional, Type, Union, + overload) import numpy as np import pyqtgraph as pg @@ -29,30 +30,17 @@ MPL_COLORS = [ key_type = Union[str, int] -class Field(QtWidgets.QWidget): +class SliderField(QtWidgets.QWidget): dtype: Type value: Any value_changed: QtCore.Signal - timer: QtCore.QTimer - - @property - def value(self) -> Any: - raise NotImplementedError() - - def values(self) -> list[Any]: - raise NotImplementedError() - - -class SliderField(Field): dtype: Type possible_values: np.ndarray _slider_max = 100 - _tuple_signal = QtCore.Signal(tuple) _int_signal = QtCore.Signal(int) _float_signal = QtCore.Signal(float) - _str_signal = QtCore.Signal(str) - def __init__(self, name: str, values: Iterable) -> None: + def __init__(self, name: str, values: Iterable[float] | Iterable[int]) -> None: super().__init__() self.__value = None self.slider = QtWidgets.QSlider(QtCore.Qt.Orientation.Horizontal) @@ -64,16 +52,18 @@ class SliderField(Field): self.field.editingFinished.connect(self.field_changed) self.step_backward_button = QtWidgets.QPushButton("<") + self.step_backward_button.setFixedWidth(30) self.step_backward_button.clicked.connect(self.step_backward) self.step_forward_button = QtWidgets.QPushButton(">") + self.step_forward_button.setFixedWidth(30) self.step_forward_button.clicked.connect(self.step_forward) self._layout = QtWidgets.QHBoxLayout() self.setLayout(self._layout) self._layout.setContentsMargins(10, 0, 10, 0) - pretty_name = " ".join(s.title() for s in name.split("_")) + pretty_name = " ".join(s for s in name.split("_")) self.name_label = QtWidgets.QLabel(pretty_name + " :") self._layout.addWidget(self.name_label) @@ -86,8 +76,6 @@ class SliderField(Field): self.value_changed = { int: self._int_signal, float: self._float_signal, - str: self._str_signal, - tuple: self._tuple_signal, }[self.dtype] self.value_changed.emit(self.__value) @@ -130,11 +118,8 @@ class SliderField(Field): self.value_changed.emit(new_value) def field_changed(self): - try: - new_val = self.dtype(self.field.text()) - except (ValueError, TypeError): - self.update_label() - return + new_val = self.dtype(self.field.text()) + new_val = min(self.value_to_slider_map, key=lambda el: abs(el - new_val)) self.value = new_val def slider_changed(self): @@ -168,7 +153,7 @@ class SliderField(Field): if self.dtype is int: return format(self.value) elif self.dtype is float: - return format(self.value, ".3g") + return format(self.value, ".5g") else: return format(self.value) @@ -176,66 +161,6 @@ class SliderField(Field): return list(self.possible_values) -class AnimatedSliderField(SliderField): - def __init__(self, name: str, values: Iterable) -> None: - super().__init__(name, values) - self.timer = QtCore.QTimer() - self.timer.timeout.connect(self.step_forward) - - self.play_button = QtWidgets.QPushButton("⏯") - self.play_button.clicked.connect(self.toggle) - self.play_button.setMaximumWidth(30) - self.playing = False - - self.interval_field = QtWidgets.QLineEdit() - self.interval_field.setMaximumWidth(60) - # self.interval_field.setValidator(QtGui.QIntValidator(1, 5000)) - self.interval_field.editingFinished.connect(self.set_interval) - self.interval_field.inputRejected.connect(self.set_interval) - self.interval = 16 - self.set_interval() - - self._layout.addWidget(self.play_button) - self._layout.addWidget(self.interval_field) - - def toggle(self): - if self.playing: - self.stop() - self.playing = False - else: - self.play() - self.playing = True - - def play(self): - if self.slider.value() == self._slider_max: - self.slider.setValue(0) - self.timer.start(self.interval) - self.play_button.setStyleSheet("QPushButton {background-color: #DEF2DD;}") - - def stop(self): - self.timer.stop() - self.play_button.setStyleSheet("QPushButton {background-color: none;}") - - def set_interval(self): - try: - self.interval = max(1, int(self.interval_field.text())) - except ValueError: - self.interval_field.setText(str(self.interval)) - if self.interval < 16: - self.increment = int(np.ceil(16 / self.interval)) - self.interval *= self.increment - else: - self.increment = 1 - - def step_forward(self): - current = self.slider.value() - if current + self.increment <= self._slider_max: - self.slider.setValue(current + self.increment) - else: - self.slider.setValue(self._slider_max) - self.stop() - - class Plot: name: str dock: Dock @@ -511,11 +436,12 @@ class PlotApp: self.params_widget = QtWidgets.QWidget() self.header_widget = QtWidgets.QWidget() self.info_label = QtWidgets.QLabel() + # self.info_label.setMaximumWidth(120) self.window.setCentralWidget(self.central_widget) self.central_layout = QtWidgets.QVBoxLayout() - self.params_layout = QtWidgets.QVBoxLayout() - self.header_layout = QtWidgets.QHBoxLayout() + self.params_layout = QtWidgets.QGridLayout() + self.header_layout = QtWidgets.QVBoxLayout() _pl = QtWidgets.QSizePolicy.Policy.Preferred info_sp = QtWidgets.QSizePolicy(_pl, _pl) @@ -540,10 +466,10 @@ class PlotApp: self.__ran = False self.params = {} - for p_name, values in params.items(): - field = AnimatedSliderField(p_name, values) + for i, (p_name, values) in enumerate(params.items()): + field = SliderField(p_name, values) self.params[p_name] = field - self.params_layout.addWidget(field) + self.params_layout.addWidget(field, *divmod(i, 2)) def set_antialiasing(self, val: bool): pg.setConfigOptions(antialias=val)