Source code for thunder4d.cell

"""Module 2: the multipass cell.

Unfolded picture used by the solver: the simulation starts right after a mirror (injection plane)
and one pass = transit over d (through the cell centre) + bounce on the next mirror, so N passes =
N transits = N bounces. Diagnostics are taken at the cell centre (waist plane of the eigenmode,
flat phase), where the modal decomposition on the cell eigenmodes is exact.

Nonlinear mode matching (aberration-free Kerr lens, uniform medium, power P constant along a pass):
the envelope equation of a gaussian becomes the linear one with lambda -> lambda sqrt(1 - P/Pc),
Pc = lambda^2/(2 pi n0 n2). Hence the matched waist and mirror spot are the linear ones scaled by
(1 - P/Pc)^(1/4), and the B-integral per pass (on axis, peak) is
    B = 8 pi n2 P / (lambda lambda_eff) * atan(d / 2 zR')      (-> 2 theta P/Pc for P << Pc).
A pulse can only be matched at one power: the wings of the pulse see the linear mode. This
time-dependent mismatch is precisely one of the couplings the 4D code resolves.
"""
import numpy as np
from scipy.constants import c as C0, femto as FS


[docs] class MPC: """Symmetric two-mirror cell. R : mirror radius of curvature [m] d : mirror separation [m] medium : Material n_passes : number of passes (transit over d + bounce = 1 pass; starts right after a mirror) mirror_gdd_fs2, mirror_tod_fs3 : dispersion per bounce mirror_reflectivity : scalar, or callable R(lambda[m]) (intensity) mirror_phase : optional callable phi(lambda[m]) [rad] per bounce (measured GDD curves, ...) mirror_diameter : clear aperture [m] (soft super-gaussian edge), None = infinite """ def __init__(self, R, d, medium, n_passes, *, mirror_gdd_fs2=0.0, mirror_tod_fs3=0.0, mirror_reflectivity=1.0, mirror_phase=None, mirror_diameter=None): if not 0 < d < 2 * R: raise ValueError("unstable cell: need 0 < d < 2R") self.R, self.d, self.medium, self.n_passes = float(R), float(d), medium, int(n_passes) self.gdd, self.tod = mirror_gdd_fs2, mirror_tod_fs3 self.refl, self.mphase = mirror_reflectivity, mirror_phase self.diameter = mirror_diameter self.g = 1 - d / R self.theta = np.arccos(self.g) # Gouy phase per pass self._bounce_cache = None
[docs] @classmethod def herriott(cls, R, N, k, medium, branch="concentric", n_passes=None, **kw): """Re-entrant Herriott cell: N passes (twice the spots per mirror), d = R (1 +- cos(2*pi k/N)). N must be even for the concentric branch. Default n_passes = N (one full re-entrant cycle); pass n_passes to simulate any other number of passes in the same cell.""" value = np.cos(2 * np.pi * k / N) if branch == "concentric": if N % 2: raise ValueError( f"N = {N} is odd: the concentric branch is only re-entrant for even N. N is the " f"number of passes of one re-entrant cycle (e.g. N = {2 * N}, k = {2 * k}); " f"to simulate a different number of passes, set n_passes separately.") d = R * (1 + value) elif branch == "planar": d = R * (1 - value) else: raise ValueError(f"unknown branch {branch!r}: use 'concentric' or 'planar'") return cls(R, d, medium, n_passes or N, **kw)
# ------------------------------------------------------------ modes
[docs] def linear_mode(self, lam): n0 = self.medium.n(2 * np.pi * C0 / lam) lm = lam / n0 w0 = np.sqrt(lm / (2 * np.pi) * np.sqrt(self.d * (2 * self.R - self.d))) wm = np.sqrt(lm * self.R / np.pi * np.sqrt(self.d / (2 * self.R - self.d))) return {"w0": w0, "w_mirror": wm, "zR": np.pi * w0 ** 2 / lm, "gouy_per_pass": self.theta}
[docs] def nonlinear_mode(self, P, lam, verbose=False): """Kerr-lens matched eigenmode for power P [W] at wavelength lam [m].""" Pc = self.medium.critical_power(lam) r = P / Pc if r >= 1: raise ValueError(f"P/Pc = {r:.2f} >= 1: no matched mode (self-focusing)") lin = self.linear_mode(lam) s = (1 - r) ** 0.25 w0, wm = lin["w0"] * s, lin["w_mirror"] * s lam_eff = lam * np.sqrt(1 - r) zRp = np.pi * w0 ** 2 / lam_eff B = 8 * np.pi * self.medium.n2 * P / (lam * lam_eff) * np.arctan(self.d / (2 * zRp)) out = {"P": P, "Pc": Pc, "P_over_Pc": r, "w0": w0, "w_mirror": wm, "w0_linear": lin["w0"], "w_mirror_linear": lin["w_mirror"], "B_per_pass": B, "B_total": B * self.n_passes, "I_peak_center_W_cm2": 2 * P / (np.pi * w0 ** 2) * 1e-4} if verbose: print(self.report(out)) return out
[docs] def match(self, pulse, power="peak", verbose=True, lam=None): """Nonlinear mode matching from a Pulse (wavelength: its grid's lambda0), or from a power in W with its wavelength lam [m]. power: 'peak' | 'weighted'.""" if hasattr(pulse, "grid"): P = pulse.peak_power() if power == "peak" else pulse.weighted_power() lam = pulse.grid.lambda0 elif lam is None: raise ValueError("match(power): give the wavelength, e.g. cell.match(5e9, lam=800e-9)") else: P = float(pulse) return self.nonlinear_mode(P, lam, verbose=verbose)
[docs] def report(self, m=None): s = (f"MPC: R = {self.R:.3f} m, d = {self.d:.4f} m, {self.n_passes} passes, " f"Gouy/pass = {np.degrees(self.theta):.2f} deg, {self.medium}") if m: s += (f"\n P = {m['P']/1e9:.3g} GW, Pc = {m['Pc']/1e9:.3g} GW, P/Pc = {m['P_over_Pc']:.3f}" f"\n waist {m['w0_linear']*1e6:.0f} um (linear) -> {m['w0']*1e6:.0f} um (matched)" f"\n mirror {m['w_mirror_linear']*1e6:.0f} um (linear) -> {m['w_mirror']*1e6:.0f} um (matched)" f"\n B/pass ~ {m['B_per_pass']:.3f} rad, total ~ {m['B_total']:.2f} rad, " f"I_peak(centre) ~ {m['I_peak_center_W_cm2']:.3g} W/cm^2") return s
# ------------------------------------------------------------ mirror
[docs] def mirror_spectrum(self, grid): """Spectral transfer of one bounce on grid.Omega: GDD, TOD, reflectivity and phase.""" W = grid.Omega * FS lam = np.where(grid.valid, grid.lam, 1.0) spec = np.exp(1j * (self.gdd / 2 * W ** 2 + self.tod / 6 * W ** 3)) spec *= np.sqrt(np.clip(self.refl(lam), 0, 1)) if callable(self.refl) else np.sqrt(self.refl) if self.mphase is not None: spec *= np.exp(1j * self.mphase(lam)) return spec * grid.valid
[docs] def mirror_aperture(self, r): """Amplitude transmission of the clear aperture at radius r (soft super-gaussian edge).""" if self.diameter is None: return np.ones_like(r) return np.exp(-(r / (self.diameter / 2)) ** 30)
[docs] def bounce_factor(self, grid): """Complex transfer of one mirror in (x, y, Om) (cached).""" if self._bounce_cache is not None and self._bounce_cache[0] is grid: return self._bounce_cache[1] F = np.exp(-1j * (grid.r2[:, :, None] * (grid.omega / C0 / self.R)[None, None, :])) F *= self.mirror_spectrum(grid)[None, None, :] F *= self.mirror_aperture(np.sqrt(grid.r2))[:, :, None] F = grid.asarray(F.astype(grid.dtype)) # built on the host, stored on the grid's device self._bounce_cache = (grid, F) return F
[docs] def bounce(self, Aw, grid): Aw *= self.bounce_factor(grid) return Aw