Source code for pynlo.utility.chi2

# -*- coding: utf-8 -*-
"""
Conversion functions and other calculators relevant to the 2nd-order nonlinear
susceptibility.

"""

__all__ = ["g2_split", "g2_shg", "domain_inversions"]


# %% Imports

import numpy as np
from scipy.constants import pi, c, epsilon_0 as e0


# %% Converters

#---- 2nd-Order Nonlinear Susceptibilities chi2 and d
def d_to_chi2(d):
    return d * 2

def chi2_to_d(chi2):
    return chi2 / 2

#---- Poling Period and Wavenumber Mismatch
def dk_to_period(dk):
    return 2*pi/dk

def period_to_dk(period):
    return 2*pi/period


# %% Nonlinearity

[docs] def g2_split(n_eff, a_eff, chi2_eff): """ The 2nd-order nonlinear parameter decomposed according to unit analysis. Parameters ---------- n_eff : array_like of float The refractive indices. a_eff : array_like of float The effective areas. chi2_eff : array_like The effective 2nd-order susceptibilities. Returns ------- ndarray (2, n) The unit decomposition of the 2nd-order nonlinear parameter. The first index contains the output factors while the second index contains the input factors. """ return np.array([1/2 * ((e0*a_eff)/(c*n_eff))**0.5 * chi2_eff, 1/(e0*c*n_eff*a_eff)**0.5])
def g2_path(n_eff, a_eff, chi2_eff, paths): """ The 2nd-order nonlinear parameter weighted for the given nonlinear pathways. Parameters ---------- n_eff : array_like of float The effective refractive indices. a_eff : array_like of float The effective areas. chi2_eff : array_like The effective 2nd-order susceptibilities. paths : list Pairs of indices for each output frequency that correspond to the input frequencies of the input paths. If no valid path exists for a particular output frequency its input indices should be given as ``[None, None]``. Returns ------- g2 : ndarray See Also -------- dominant_paths : Determine the dominant paths based on phase mismatch. """ g2s = g2_split(n_eff, a_eff, chi2_eff) assert g2s.size != len(paths), "The number of paths must equal the number of frequencies." g2_p = [] for idx, path in enumerate(paths): if None in path: g2_p.append(0.0) else: g2 = np.mean([g2s[0, idx] * g2s[1, cpl[0]]*g2s[1, cpl[0]] for cpl in path]) g2_p.append(g2) return np.arary(g2_p)
[docs] def g2_shg(v0, v_grid, n_eff, a_eff, chi2_eff): """ The 2nd-order nonlinear parameter weighted for second harmonic generation centered around the given input frequency. Parameters ---------- v0 : float The target fundamental frequency to be doubled. v_grid : array_like of float The frequency grid. n_eff : array_like of float The effective refractive indices. a_eff : array_like of float The effective areas. chi2_eff : array_like The effective 2nd-order susceptibilities. Returns ------- g2 : ndarray """ g2_out, g2_in = g2_split(n_eff, a_eff, chi2_eff) v1_idx = np.argmin(np.abs(v_grid - v0)) v2_idx = np.argmin(np.abs(v_grid - 2*v0)) vc_idx = (v1_idx + v2_idx)//2 # crossover point g2_in_fh = np.interp(2*v_grid, v_grid, g2_in) # input to fundamental harmonic g2_in_sh = np.interp(0.5*v_grid, v_grid, g2_in) # input to second harmonic # Fundamental (DFG) g2_fh = g2_out * g2_in_fh * g2_in # Second Harmonic (SFG) g2_sh = g2_out * g2_in_sh**2 # Crossover g2 = np.zeros_like(g2_out) g2[:vc_idx] = g2_fh[:vc_idx] g2[vc_idx:] = g2_sh[vc_idx:] return g2
def g2_sfg(v0, v_grid, n_eff, a_eff, chi2_eff): """ The 2nd-order nonlinear parameter weighted for sum frequency generation driven by the given input frequency. Parameters ---------- v0 : float The target pump frequency. v_grid : array_like of float The frequency grid. n_eff : array_like of float The effective refractive indices. a_eff : array_like of float The effective areas. chi2_eff : array_like The effective 2nd-order susceptibilities. Returns ------- g2 : ndarray """ g2_out, g2_in = g2_split(n_eff, a_eff, chi2_eff) vc_idx = np.argmin(np.abs(v_grid - (v_grid.min() + v0))) # crossover point g2_in_v0 = np.interp(v0, v_grid, g2_in) g2_in_sf = np.interp(v_grid - v0, v_grid, g2_in) # input to sum (out = in + v0) g2_in_df = np.interp(v_grid + v0, v_grid, g2_in) # input to diff (out = in - v0) # Fundamental (DFG) g2_sf = g2_out * g2_in_sf * g2_in_v0 # Second Harmonic (SFG) g2_df = g2_out * g2_in_df * g2_in_v0 # Crossover g2 = np.zeros_like(g2_out) g2[:vc_idx] = g2_df[:vc_idx] g2[vc_idx:] = g2_sf[vc_idx:] return g2 # %% Phase Matching
[docs] def domain_inversions(z, dk, n=1): """ Find the location of the domain inversion boundaries that quasi-phase match (QPM) the given wavenumber mismatch. Parameters ---------- z : float or array_like of float The propagation distance. dk : float or array_like of float The wavenumber mismatch, or ``2*pi/polling period``. n : int, optional The interpolation order used to calculate the inversion points. Returns ------- z_invs : ndarray of float The location of all domain inversion boundaries. domains : ndarray of float The length of each domain. poled : ndarray of int The poling status of each domain. A value of 1 indicates an inverted domain. A value of 0 indicates an unpoled region. Notes ----- For continuous QPM profiles, the domain boundaries can be calculated by inverting the integral equation that gives the accumulated phase mismatch: .. math:: 2 \\pi N[z] &= \\int_{z_0}^z \\Delta k[z^\\prime] dz^\\prime = \\int_{z_0}^z \\frac{2 \\pi}{\\Lambda[z^\\prime]} dz^\\prime \\\\ \\text{z}_{inv}[n] &= N^{-1}[n/2] where :math:`\Delta k` is the wavenumber mismatch compensated by poling period :math:`\Lambda`, :math:`N` is the accumulated number of phase cycles, and :math:`n` is an integer. """ from scipy.interpolate import InterpolatedUnivariateSpline #---- Process Input assert 1 <= n <= 5, "The interpolation order must be between 1 and 5." z = np.asarray(z) dk = np.abs(np.asarray(dk)) aperiodic = dk.size > 1 if aperiodic: # Aperiodic Poling assert np.all(np.diff(z) > 0), "The points in `z` must be ordered monotonically." assert z.size == dk.size, "`dk` must have the same number of points as `z`." assert z.size > n, "The number of points must be greater than the interpolation order." else: # Periodic Poling n = 1 # linear interpolation is exact if z.size == 1: assert z > 0, "The poled length must be greater than 0." z = np.linspace(0, z, n+1) dk = np.ones_like(z) * dk #---- Inversion Points n_cycles = InterpolatedUnivariateSpline(z, dk/(2*pi), k=n).antiderivative() if aperiodic and n == 1: # Linearly interpolate dk, quadratically interpolate z_n n = 2 if not z.size > n: z = np.linspace(z.min(), z.max(), n+1) z_n = InterpolatedUnivariateSpline(2*n_cycles(z), z, k=n) n_invs = int(2*n_cycles(z[-1])) # round down n_grid = np.arange(n_invs + 1) z_invs = z_n(n_grid) # all inversion points #---- Domains poled = n_grid % 2 domains = np.diff(z_invs) if np.isclose(z_invs.max(), z.max(), rtol=0, atol=10e-9): # Ignore final inversion if at the end poled = poled[:-1] z_invs = z_invs[:-1] else: # Include final domain domains = np.append(domains, z.max() - z_invs.max()) return z_invs[1:], domains, poled
def dominant_paths(v_grid, beta, beta_qpm=None, full=False): #TODO: add extent (for imshow) """ For each output frequency, find the input frequencies that are coupled with the least phase mismatch. This function checks both sum frequency generation (SFG) pathways and difference frequency generation (DFG) pathways. Parameters ---------- v_grid : array_like of float The frequency grid. beta : array_like of float The wavenumber of each input frequency. beta_qpm : float, optional The effective wavenumber of an applied quasi-phase matching (QPM) structure. If ``None``, the dominant path is calculated without poling. full : bool, optional A flag determining the nature of the return value. When ``False`` (the default) just the path indices are returned, when ``True`` the calculated wavenumber mismatch arrays are also returned. Returns ------- paths : list Pairs of indices for each output frequency that correspond to the input frequencies of the dominant input paths. If no valid path exists for a particular output frequency its input indices are given as ``[None, None]``. (dk, dk_sfg, dk_dfg) : tuple These values are only returned if ``full=True`` \n dk : list The wavenumber mismatch for each path in `paths`. v_sfg : ndarray of float The frequencies that correspond to all SFG combinations. dk_sfg : ndarray of float The wavenumber mismatch for all SFG combinations. The mismatch of invalid paths are given as ``np.nan``. v_dfg : ndarray of float The frequencies that correspond to all DFG combinations. dk_dfg : ndarray of float The wavenumber mismatch for all DFG combinations. The mismatch of invalid paths are given as NaN. """ #---- Setup v_grid = np.asarray(v_grid, dtype=float) n = v_grid.size v_idx = np.arange(n) v_idx2 = np.dstack(np.indices((n,n))) beta = np.asarray(beta, dtype=float) #---- Sum Frequency v_sfg = np.add.outer(v_grid, v_grid) # Indexing v_idx_sfg = np.interp(v_sfg, v_grid, v_idx, left=np.nan, right=np.nan).round() sfg_idxs = np.arange(np.nanmin(v_idx_sfg), np.nanmax(v_idx_sfg)+1, dtype=int) sfg_diag_offset = (n-1) - np.arange(sfg_idxs.size) # Wavenumber Mismatch beta_sfg_12 = np.add.outer(beta, beta) beta_sfg_3 = np.interp(v_sfg, v_grid, beta, left=np.nan, right=np.nan) if beta_qpm is None: dk_sfg = np.abs(beta_sfg_12 - beta_sfg_3) else: dk_sfg = np.abs(np.abs(beta_sfg_12 - beta_sfg_3) - beta_qpm) #---- Difference Frequency v_dfg = np.subtract.outer(v_grid, v_grid) # Indexing v_idx_dfg = np.interp(v_dfg, v_grid, v_idx, left=np.nan, right=np.nan).round() dfg_idxs = np.arange(np.nanmin(v_idx_dfg), np.nanmax(v_idx_dfg)+1, dtype=int) dfg_diag_offset = -(n-1) + np.arange(dfg_idxs.size)[::-1] # Wavenumber Mismatch beta_dfg_31 = np.subtract.outer(beta, beta) beta_dfg_2 = np.interp(v_dfg, v_grid, beta, left=np.nan, right=np.nan) if beta_qpm is None: dk_dfg = np.abs(beta_dfg_31 - beta_dfg_2) else: dk_dfg = np.abs(np.abs(beta_dfg_31 - beta_dfg_2) - beta_qpm) #---- Paths of Minimum Phase Mismatch paths = [] dk = [] for idx in v_idx: valid_sfg = (idx in sfg_idxs) valid_dfg = (idx in dfg_idxs) # SFG if valid_sfg: diag_offset = sfg_diag_offset[idx==sfg_idxs][0] sfg_diag = np.fliplr(dk_sfg).diagonal(diag_offset) min_dk_sfg = np.nanmin(sfg_diag) min_dk_sfg_idx = np.nonzero(sfg_diag==min_dk_sfg) sfg_path = np.fliplr(v_idx2).diagonal(diag_offset).T[min_dk_sfg_idx] # DFG if valid_dfg: diag_offset = dfg_diag_offset[idx==dfg_idxs][0] dfg_diag = dk_dfg.diagonal(diag_offset) min_dk_dfg = np.nanmin(dfg_diag) min_dk_dfg_idx = np.nonzero(dfg_diag==min_dk_dfg) dfg_path = v_idx2.diagonal(diag_offset).T[min_dk_dfg_idx] if valid_sfg and valid_dfg: if min_dk_sfg < min_dk_dfg: # SFG smaller paths.append(sfg_path.tolist()) elif min_dk_sfg > min_dk_dfg: # DFG smaller paths.append(dfg_path.tolist()) else: # Equal paths.append(sfg_path.tolist() + dfg_path.tolist()) dk.append(min_dk_sfg) if min_dk_sfg < min_dk_dfg else dk.append(min_dk_dfg) elif valid_sfg: # Only SFG paths.append(sfg_path.tolist()) dk.append(min_dk_sfg) elif valid_dfg: # Only DFG paths.append(dfg_path.tolist()) dk.append(min_dk_dfg) else: # No path paths.append([[None, None]]) dk.append(None) if full: return paths, (dk, v_sfg, dk_sfg, v_dfg, dk_dfg) else: return paths # %% Calculator Functions def effective_chi3(): pass #TODO: effective 3rd-order from cascaded 2nd def shg_conversion_efficiency(v0, p0, n_v, n_2v, a_eff, d_eff, z, qpm_order=None): """ Conversion efficiency of second-harmonic generation in the undepleted-pump approximation. Parameters ---------- v0 : float The fundamental frequency. p0 : float The average power. n_v : float The refractive index at the fundamental frequency. n_2v : float The refractive index at the second-harmonic frequency. a_eff : float The effective area. d_eff : float The effective second order nonlinearity. z : float The propagation distance. qpm_order : int The order of the quasi-phase-matching. Only odd orders can be quasi-phase matched. References ---------- Robert W. Boyd, Nonlinear Optics (Fourth Edition), Academic Press, 2020 https://doi.org/10.1016/C2015-0-05510-1 """ w0 = 2*pi*v0 if qpm_order is not None: d_eff = d_eff * np.abs(np.sinc(qpm_order/2)) return (2*d_eff**2 * w0**2 * p0)/(n_v**2 * n_2v * a_eff * e0 * c**3) * z**2