Files
dispersionapp/dispersion_app.py
2023-03-17 12:16:50 +01:00

237 lines
7.8 KiB
Python
Executable File

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, ValidationError, confloat
DEFAULT_CONFIG_FILE = "config.toml"
class Config(BaseModel):
wl_min: confloat(ge=100, le=1000)
wl_max: confloat(ge=500, le=6000)
wl_pump: confloat(ge=200, le=6000)
rep_rate: confloat(gt=0)
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
try:
return cls(**d)
except ValidationError as e:
s = f"invalid input in config file {config_file}:\n{e}"
print(s)
sys.exit(1)
@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()}",
core_diameter_um=np.linspace(50, 300),
pressure_mbar=np.geomspace(1, 2000),
wall_thickness_um=np.geomspace(0.01, 10),
n_tubes=np.arange(6, 16),
gap_um=np.linspace(1, 15),
t_fwhm_fs=np.linspace(10, 200, 96),
) 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], [-3, 3])
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()