commit 71ec5dd0af4061c60fad8d566ceb18e249982cce Author: BenoƮt Sierro Date: Fri Mar 17 11:17:18 2023 +0100 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d5b7e20 --- /dev/null +++ b/.gitignore @@ -0,0 +1,162 @@ +pyrightconfig.json + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ diff --git a/config.toml b/config.toml new file mode 100644 index 0000000..9d79bf2 --- /dev/null +++ b/config.toml @@ -0,0 +1,7 @@ +wl_min = 160 +wl_max = 1600 +wl_pump = 800 + +rep_rate = 8e3 + +safety_factor = 10 diff --git a/dispersion_app.py b/dispersion_app.py new file mode 100755 index 0000000..82f8981 --- /dev/null +++ b/dispersion_app.py @@ -0,0 +1,231 @@ +from __future__ import annotations + +import os +import sys +import warnings +from functools import cache +from typing import Any, NamedTuple + +import click +import numpy as np +import scgenerator as sc +import tomli +from customfunc.app import PlotApp +from pydantic import BaseModel + +DEFAULT_CONFIG_FILE = "config.toml" + + +class Config(BaseModel): + wl_min: float + wl_max: float + wl_pump: float + rep_rate: float + gas: str + + @classmethod + def load(cls, config_file: str | None = None) -> Config: + config_file = config_file or DEFAULT_CONFIG_FILE + with open(config_file, "rb") as file: + d = tomli.load(file) + d = cls.default() | d + return cls(**d) + + @classmethod + def default(cls) -> dict[str, Any]: + return dict(wl_min=160, wl_max=1600, wl_pump=800, rep_rate=8e3, gas="argon") + + +class LimitValues(NamedTuple): + wl_zero_disp: float + ion_lim: float + sf_lim: float + + +def b2(w, n_eff): + dw = w[1] - w[0] + beta = sc.fiber.beta(w, n_eff) + return sc.math.differentiate_arr(beta, 2, 4, dw) + + +def N_sf_max( + wl: np.ndarray, t0: float, wl_zero_disp: float, gas: sc.materials.Gas, safety: float = 10.0 +) -> np.ndarray: + """ + maximum soliton number according to self focusing + + eq. S15 in Travers2019 + """ + delta_gas = gas.sellmeier.delta(wl, wl_zero_disp) + return t0 * np.sqrt(wl / (safety * np.abs(delta_gas))) + + +def N_ion_max( + wl: np.ndarray, t0: float, wl_zero_disp: float, gas: sc.materials.Gas, safety: float = 10.0 +) -> np.ndarray: + """ + eq. S16 in Travers2019 + """ + ind = sc.math.argclosest(wl, wl_zero_disp) + f = np.gradient(np.gradient(gas.sellmeier.chi(wl), wl), wl) + factor = (sc.math.u_nm(1, 1) / sc.units.c) ** 2 * (0.5 * wl / np.pi) ** 3 + delta = factor * (f / f[ind] - 1) + denom = safety * np.pi * wl * np.abs(delta) * f[ind] + return t0 * sc.math.u_nm(1, 1) * np.sqrt(gas.n2() * gas.barrier_suppression / denom) + + +def solition_num( + t0: float, w0: float, beta2: float, n2: float, core_radius: float, peak_power: float +) -> float: + gamma = sc.fiber.gamma_parameter(n2, w0, sc.fiber.A_eff_marcatili(core_radius)) + ld = sc.pulse.L_D(t0, beta2) + return np.sqrt(gamma * ld * peak_power) + + +def energy( + t0: float, w0: float, beta2: float, n2: float, core_radius: float, solition_num: float +) -> float: + gamma = sc.fiber.gamma_parameter(n2, w0, sc.fiber.A_eff_marcatili(core_radius)) + peak_power = solition_num**2 * abs(beta2) / (t0**2 * gamma) + return sc.pulse.P0_to_E0(peak_power, t0, "sech") + + +def app(config_file: os.PathLike | None = None): + config = Config.load(config_file) + # grid stuff + w = sc.w_from_wl(config.wl_min, config.wl_max, 2048) + wl = sc.units.m.inv(w) + 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()}", + wall_thickness_um=np.geomspace(0.01, 10), + core_diameter_um=np.linspace(50, 300), + pressure_mbar=np.geomspace(1, 2000), + n_tubes=np.arange(6, 16), + gap_um=np.linspace(1, 15), + t_fwhm_fs=np.linspace(10, 200), + ) as app: + # initial setup + app[0].horizontal_line("reference", 0, color="gray") + app[0].set_xlabel("wavelength (nm)") + app[0].set_ylabel("beta2 (fs^2/cm)") + app.params["wall_thickness_um"].value = 1 + app.params["core_diameter_um"].value = 100 + app.params["pressure_mbar"].value = 500 + app.params["n_tubes"].value = 7 + app.params["gap_um"].value = 5 + app.params["t_fwhm_fs"].value = 100 + app[0].set_lim(ylim=(-4, 2)) + + @cache + def compute_max_energy( + core_diameter_um: float, pressure_mbar: float, t_fwhm_fs: float + ) -> LimitValues: + t_fwhm = 1e-15 * t_fwhm_fs + pressure = 1e2 * pressure_mbar + core_radius = 0.5e-6 * core_diameter_um + t0 = sc.pulse.fwhm_to_T0_fac["sech"] * t_fwhm + + disp = compute_capillary(core_diameter_um, pressure_mbar) + wl_zero_disp = sc.math.all_zeros(wl, disp) + if len(wl_zero_disp) == 0: + return + wl_zero_disp = wl_zero_disp[0] + + with warnings.catch_warnings(): + warnings.simplefilter("ignore") + ion_limit = N_ion_max(wl, t0, wl_zero_disp, gas)[wl_ind] + sf_limit = N_sf_max(wl, t0, wl_zero_disp, gas)[wl_ind] + + beta2 = disp[wl_ind] + n2 = gas.n2(pressure=pressure) + + return LimitValues( + wl_zero_disp, + energy(t0, w0, beta2, n2, core_radius, ion_limit), + energy(t0, w0, beta2, n2, core_radius, sf_limit), + ) + + @cache + def compute_vincetti( + wall_thickness_um: float, + core_diameter_um: float, + pressure_mbar: float, + 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 + pressure = pressure_mbar * 1e2 + + 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 + ) + return b2(w, n_eff_vinc) + + @cache + def compute_capillary(core_diameter_um: float, pressure_mbar: float) -> np.ndarray: + pressure = pressure_mbar * 1e2 + core_diameter = core_diameter_um * 1e-6 + n_gas_2 = gas.sellmeier.n_gas_2(wl, None, pressure) + n_eff_cap = sc.fiber.n_eff_marcatili(wl, n_gas_2, core_diameter / 2) + return b2(w, n_eff_cap) + + @app.update + def draw_vincetty( + wall_thickness_um: float, + core_diameter_um: float, + pressure_mbar: float, + n_tubes: int, + gap_um: float, + ): + b2 = compute_vincetti( + wall_thickness_um, core_diameter_um, pressure_mbar, n_tubes, gap_um + ) + app[0].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) + app[0].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): + lim = compute_max_energy(core_diameter_um, pressure_mbar, t_fwhm_fs) + if lim.ion_lim > lim.sf_lim: + power = lim.sf_lim * 1e3 * config.rep_rate + app[0].set_line_name( + "Capillary", f"Capillary, max energy = {power:.0f}mW (self-focusing)" + ) + else: + power = lim.ion_lim * 1e3 * config.rep_rate + app[0].set_line_name( + "Capillary", f"Capillary, max energy = {power:.0f}mW (ionization)" + ) + + zdw = lim.wl_zero_disp * 1e9 + app[0].set_line_data("zdw", [zdw, zdw], [0, 1]) + app[0].set_line_name("zdw", f"ZDW = {zdw:.0f}nm") + + +@click.command() +@click.option( + "-c", + "--config", + default=DEFAULT_CONFIG_FILE, + help="configuration file in TOML format", + type=click.Path(exists=True, file_okay=True, dir_okay=False, resolve_path=True), +) +def main(config: os.PathLike): + app(config) + + +if __name__ == "__main__" and not hasattr(sys, "ps1"): + main()