LCOV - code coverage report
Current view: top level - src/pairinteraction/green_tensor - dynamic_green_tensor.py (source / functions) Hit Total Coverage
Test: coverage.info Lines: 141 186 75.8 %
Date: 2026-08-14 15:26:44 Functions: 8 17 47.1 %

          Line data    Source code
       1             : # SPDX-FileCopyrightText: 2024 PairInteraction Developers
       2             : # SPDX-License-Identifier: LGPL-3.0-or-later
       3             : 
       4             : # ruff: noqa: N802, N806
       5             : 
       6           1 : from __future__ import annotations
       7             : 
       8           1 : import math
       9           1 : from typing import TYPE_CHECKING, Literal, TypeVar
      10             : 
      11           1 : import numpy as np
      12           1 : import scipy.constants as const
      13           1 : from numba import njit
      14           1 : from scipy.integrate import quad
      15             : 
      16           1 : from pairinteraction.green_tensor.bessel_function import (
      17             :     cached_bessel_function_0,
      18             :     cached_bessel_function_1,
      19             :     cached_bessel_function_2,
      20             : )
      21             : 
      22             : if TYPE_CHECKING:
      23             :     from collections.abc import Callable
      24             : 
      25             :     from pairinteraction.units import NDArray
      26             : 
      27             :     Entries = Literal["xx", "xy", "xz", "yx", "yy", "yz", "zx", "zy", "zz"]
      28             : 
      29             :     from typing_extensions import ParamSpec
      30             : 
      31             :     P = ParamSpec("P")
      32             :     R = TypeVar("R")
      33             : 
      34             :     def njit(cache: bool) -> Callable[[Callable[P, R]], Callable[P, R]]: ...  # type: ignore [no-redef]
      35             : 
      36             : 
      37           1 : __all__ = ["dynamic_green_tensor_homogeneous", "dynamic_green_tensor_scattered"]
      38             : 
      39             : 
      40           1 : def dynamic_green_tensor_homogeneous(
      41             :     pos1: NDArray, pos2: NDArray, omega: float, epsilon0: complex, *, only_real_part: bool = False
      42             : ) -> NDArray:
      43             :     r"""Homogeneous Green Tensor for two atoms in cartesian coordinates in an infinite homogeneous medium.
      44             : 
      45             :     The function used is from equation 2 of the paper:
      46             :     "Dispersionless subradiant photon storage in one-dimensional emitter chains"
      47             :     https://doi.org/10.1103/PhysRevA.108.L051702
      48             : 
      49             :     We calculate the scaled Green Tensor, i.e.
      50             :     .. math::
      51             :         \frac{\omega^2}{\hbar \epsilon_0 c^2} G(r_\alpha, r_\beta, \omega)
      52             : 
      53             :     Args:
      54             :         pos1: Position vector of atom A in meters
      55             :         pos2: Position vector of atom B in meters
      56             :         omega: Angular frequency (i.e. 2*pi*f) in 1/s
      57             :         epsilon0: Electric permittivity of the medium (dimensionless, complex)
      58             :         only_real_part: If True, only the real part of the Green tensor is calculated (default: False)
      59             : 
      60             :     Returns: The 3x3 scaled homogeneous Green Tensor (general complex values) (m^(-3) [hbar]^(-1) [epsilon_0]^(-1))
      61             :         :math:`\omega^2 / (\hbar \epsilon_0 c^2) G(r_\alpha, r_\beta, \omega)`
      62             : 
      63             :     """
      64           1 :     k_vac = omega / const.c  # magnitude of wave vector in vacuum
      65           1 :     k0 = k_vac * np.sqrt(epsilon0)  # magnitude of wave vector in medium with permittivity epsilon0
      66           1 :     distance_vec = pos2 - pos1
      67           1 :     distance = np.linalg.norm(distance_vec)
      68             : 
      69             :     # this is missing a 1/k0**2 compared to the paper, we absorb this in the scaled prefactor, see line below
      70           1 :     prefactor = np.exp(1j * k0 * distance) / (4 * np.pi * distance**3)
      71           1 :     prefactor *= 1 / (epsilon0 * const.epsilon_0 * const.hbar)  # epsilon0 from missing k0^2 compared to omega^2/c^2
      72           1 :     prefactor *= -1  # minus sign from H = - \hbar \sum V_{\alpha\beta} ...
      73             : 
      74           1 :     result: NDArray = prefactor * (
      75             :         (k0**2 * distance**2 + 1j * k0 * distance - 1) * np.eye(3)
      76             :         + (-(k0**2) * distance**2 - 3j * k0 * distance + 3) * np.outer(distance_vec, distance_vec) / distance**2
      77             :     )
      78             : 
      79           1 :     if only_real_part:
      80           1 :         return np.real(result)
      81           0 :     return result
      82             : 
      83             : 
      84           1 : def dynamic_green_tensor_scattered(
      85             :     pos1: NDArray,
      86             :     pos2: NDArray,
      87             :     z1: float,
      88             :     z2: float,
      89             :     omega: float,
      90             :     epsilon0: complex,
      91             :     epsilon1: complex,
      92             :     epsilon2: complex,
      93             :     *,
      94             :     only_real_part: bool = False,
      95             : ) -> NDArray:
      96             :     """Assemble the total scattering Green tensor.
      97             : 
      98             :     Args:
      99             :         pos1: Position vector of atom A (m)
     100             :         pos2: Position vector of atom B (m)
     101             :         z1: z-coordinate of the first surface (m)
     102             :         z2: z-coordinate of the second surface (m)
     103             :         omega: Angular frequency (i.e. 2*pi*f) in 1/s
     104             :         epsilon0: Electric permittivity of the medium between the two surfaces (dimensionless, complex)
     105             :         epsilon1: Electric permittivity of the upper medium (dimensionless, complex)
     106             :         epsilon2: Electric permittivity of the lower medium (dimensionless, complex)
     107             :         only_real_part: If True, only the real part of the Green tensor is calculated (default: False)
     108             : 
     109             :     Returns: The 3x3 Scattering Green Tensor (general complex values) (1/m)
     110             : 
     111             :     """
     112           1 :     if z1 > z2:
     113             :         # Ensure z1 is the lower surface and z2 is the upper surface
     114           1 :         z1, z2 = z2, z1
     115           1 :         epsilon1, epsilon2 = epsilon2, epsilon1
     116           1 :     if not (z1 < pos1[2] < z2 and z1 < pos2[2] < z2):
     117           0 :         raise ValueError("Both atoms must be located between the two surfaces (i.e. z1 < z_atom < z2).")
     118             : 
     119           1 :     distance = pos2 - pos1
     120           1 :     height = abs(z1 - z2)
     121             : 
     122           1 :     rho = np.sqrt(distance[0] ** 2 + distance[1] ** 2)
     123           1 :     phi = np.atan2(distance[1], distance[0]) if rho != 0 else 0
     124             : 
     125           1 :     z_ges = pos1[2] + pos2[2] - 2 * min(z1, z2)
     126           1 :     z_diff = pos1[2] - pos2[2]
     127             : 
     128           1 :     gt_scattered = np.zeros((3, 3), dtype=complex)
     129           1 :     for i, ix in enumerate(["x", "y", "z"]):
     130           1 :         for j, jx in enumerate(["x", "y", "z"]):
     131           1 :             entry: Entries = ix + jx  # type: ignore [assignment]
     132           1 :             g_ij_elliptic = elliptic_integral(
     133             :                 omega,
     134             :                 height,
     135             :                 rho,
     136             :                 phi,
     137             :                 epsilon0,
     138             :                 epsilon1,
     139             :                 epsilon2,
     140             :                 z_ges,
     141             :                 z_diff,
     142             :                 entry,
     143             :                 only_real_part=only_real_part,
     144             :             )
     145           1 :             g_ij_real = real_axis_integral(
     146             :                 omega,
     147             :                 height,
     148             :                 rho,
     149             :                 phi,
     150             :                 epsilon0,
     151             :                 epsilon1,
     152             :                 epsilon2,
     153             :                 z_ges,
     154             :                 z_diff,
     155             :                 entry,
     156             :                 only_real_part=only_real_part,
     157             :             )
     158             :             # prefactor see comment in dynamic_green_tensor_homogeneous
     159           1 :             prefactor = -1 / (epsilon0 * const.epsilon_0 * const.hbar)
     160           1 :             value = prefactor * (g_ij_elliptic + g_ij_real)
     161           1 :             gt_scattered[i][j] = value
     162             : 
     163           1 :     return gt_scattered
     164             : 
     165             : 
     166           1 : @njit(cache=True)
     167           1 : def branch(epsilon: complex, k: float, k_rho: complex) -> complex:
     168             :     """Calculate the perpendicular wave vector component with positive imaginary part.
     169             : 
     170             :     Args:
     171             :         epsilon: Electric permittivity of the medium (dimensionless, complex)
     172             :         k: Magnitude of wave number in vacuum (1/m)
     173             :         k_rho: In-plane wave vector component (1/m)
     174             : 
     175             :     Returns: The perpendicular wave vector component (1/m)
     176             : 
     177             :     """
     178           0 :     return np.sqrt(epsilon * k**2 - k_rho**2 + 0j)  # type: ignore [no-any-return]
     179             : 
     180             : 
     181           1 : """The following functions are used from Appendix B of the paper:
     182             : "Modified dipole-dipole interaction and dissipation in an atomic ensemble near surfaces"
     183             : https://doi.org/10.1103/PhysRevA.97.053841
     184             : and are needed to calculate the scattering Green Tensor for two atoms between two planar surfaces.
     185             : We try to keep the same notation (in particular the same function names) as in the paper.
     186             : """
     187             : 
     188             : 
     189           1 : @njit(cache=True)
     190           1 : def rs(kz: complex, k1z: complex) -> complex:
     191             :     """Calculate the Fresnel reflection coefficient for s-polarized light.
     192             : 
     193             :     Args:
     194             :         kz: Perpendicular wave vector component in the medium between the two surfaces (1/m)
     195             :         k1z: Perpendicular wave vector component in the upper or lower medium (1/m)
     196             : 
     197             :     Returns: The Fresnel reflection coefficient for s-polarized light (dimensionless, complex)
     198             : 
     199             :     """
     200           0 :     if kz == 0 and k1z == 0:
     201           0 :         return 0.0
     202           0 :     return (kz - k1z) / (kz + k1z)
     203             : 
     204             : 
     205           1 : @njit(cache=True)
     206           1 : def rp(kz: complex, k1z: complex, epsilon: complex) -> complex:
     207             :     """Calculate the Fresnel reflection coefficient for p-polarized light.
     208             : 
     209             :     Args:
     210             :         kz: Perpendicular wave vector component in the medium between the two surfaces (1/m)
     211             :         k1z: Perpendicular wave vector component in the upper or lower medium (1/m)
     212             :         epsilon: Electric permittivity of the upper or lower medium (dimensionless, complex)
     213             : 
     214             :     Returns: The Fresnel reflection coefficient for p-polarized light (dimensionless, complex)
     215             : 
     216             :     """
     217           0 :     if kz == 0 and k1z == 0:
     218           0 :         return 0.0
     219           0 :     return (epsilon * kz - k1z) / (epsilon * kz + k1z)
     220             : 
     221             : 
     222           1 : @njit(cache=True)
     223           1 : def D(r_plus: complex, r_minus: complex, kz: complex, h: float) -> complex:
     224             :     """Calculate the denominator term D used in the scattering Green Tensor matrix elements.
     225             : 
     226             :     Args:
     227             :         r_plus: Fresnel reflection coefficient for the upper surface (dimensionless, complex)
     228             :         r_minus: Fresnel reflection coefficient for the lower surface (dimensionless, complex)
     229             :         kz: Perpendicular wave vector component in the medium between the two surfaces (1/m)
     230             :         h: Distance between the two surfaces (m)
     231             : 
     232             :     Returns: The value of the denominator term D (dimensionless, complex)
     233             : 
     234             :     """
     235           0 :     return 1 - r_plus * r_minus * np.exp(2j * kz * h)  # type: ignore [no-any-return]
     236             : 
     237             : 
     238           1 : @njit(cache=True)
     239           1 : def A_plus(r_plus: complex, r_minus: complex, kz: complex, h: float, z_ges: float, z_diff: float) -> complex:
     240             :     """Calculate the numerator term A_plus used in the scattering Green Tensor matrix elements.
     241             : 
     242             :     Args:
     243             :         r_plus: Fresnel reflection coefficient for the upper surface (dimensionless, complex)
     244             :         r_minus: Fresnel reflection coefficient for the lower surface (dimensionless, complex)
     245             :         kz: Perpendicular wave vector component in the medium between the two surfaces (1/m)
     246             :         h: Distance between the two surfaces (m)
     247             :         z_ges: Total z-coordinate (m)
     248             :         z_diff: Difference in z-coordinates (m)
     249             : 
     250             :     Returns: The value of the numerator term A_plus (dimensionless, complex)
     251             : 
     252             :     """
     253           0 :     return (  # type: ignore [no-any-return]
     254             :         r_minus * np.exp(1j * kz * (z_ges - h))
     255             :         + r_plus * np.exp(-1j * kz * (z_ges - h))
     256             :         + 2 * r_plus * r_minus * np.cos(kz * z_diff) * np.exp(1j * kz * h)
     257             :     ) / D(r_plus, r_minus, kz, h)
     258             : 
     259             : 
     260           1 : @njit(cache=True)
     261           1 : def A_minus(r_plus: complex, r_minus: complex, kz: complex, h: float, z_ges: float, z_diff: float) -> complex:
     262             :     """Calculate the numerator term A_minus used in the scattering Green Tensor matrix elements.
     263             : 
     264             :     Args:
     265             :         r_plus: Fresnel reflection coefficient for the upper surface (dimensionless, complex)
     266             :         r_minus: Fresnel reflection coefficient for the lower surface (dimensionless, complex)
     267             :         kz: Perpendicular wave vector component in the medium between the two surfaces (1/m)
     268             :         h: Distance between the two surfaces (m)
     269             :         z_ges: Total z-coordinate (m)
     270             :         z_diff: Difference in z-coordinates (m)
     271             : 
     272             :     Returns: The value of the numerator term A_minus (dimensionless, complex)
     273             : 
     274             :     """
     275           0 :     return (  # type: ignore [no-any-return]
     276             :         r_minus * np.exp(1j * kz * (z_ges - h))
     277             :         + r_plus * np.exp(-1j * kz * (z_ges - h))
     278             :         - 2 * r_plus * r_minus * np.cos(kz * z_diff) * np.exp(1j * kz * h)
     279             :     ) / D(r_plus, r_minus, kz, h)
     280             : 
     281             : 
     282           1 : @njit(cache=True)
     283           1 : def B_plus(r_plus: complex, r_minus: complex, kz: complex, h: float, z_ges: float, z_diff: float) -> complex:
     284             :     """Calculate the numerator term B_plus used in the scattering Green Tensor matrix elements.
     285             : 
     286             :     Args:
     287             :         r_plus: Fresnel reflection coefficient for the upper surface (dimensionless, complex)
     288             :         r_minus: Fresnel reflection coefficient for the lower surface (dimensionless, complex)
     289             :         kz: Perpendicular wave vector component in the medium between the two surfaces (1/m)
     290             :         h: Distance between the two surfaces (m)
     291             :         z_ges: Total z-coordinate (m)
     292             :         z_diff: Difference in z-coordinates (m)
     293             : 
     294             :     Returns: The value of the numerator term B_plus (dimensionless, complex)
     295             : 
     296             :     """
     297           0 :     return (  # type: ignore [no-any-return]
     298             :         r_minus * np.exp(1j * kz * (z_ges - h))
     299             :         - r_plus * np.exp(-1j * kz * (z_ges - h))
     300             :         + 2j * r_plus * r_minus * np.sin(kz * z_diff) * np.exp(1j * kz * h)
     301             :     ) / D(r_plus, r_minus, kz, h)
     302             : 
     303             : 
     304           1 : @njit(cache=True)
     305           1 : def B_minus(r_plus: complex, r_minus: complex, kz: complex, h: float, z_ges: float, z_diff: float) -> complex:
     306             :     """Calculate the numerator term B_minus used in the scattering Green Tensor matrix elements.
     307             : 
     308             :     Args:
     309             :         r_plus: Fresnel reflection coefficient for the upper surface (dimensionless, complex)
     310             :         r_minus: Fresnel reflection coefficient for the lower surface (dimensionless, complex)
     311             :         kz: Perpendicular wave vector component in the medium between the two surfaces (1/m)
     312             :         h: Distance between the two surfaces (m)
     313             :         z_ges: Total z-coordinate (m)
     314             :         z_diff: Difference in z-coordinates (m)
     315             : 
     316             :     Returns: The value of the numerator term B_minus (dimensionless, complex)
     317             : 
     318             :     """
     319           0 :     return (  # type: ignore [no-any-return]
     320             :         r_minus * np.exp(1j * kz * (z_ges - h))
     321             :         + r_plus * np.exp(-1j * kz * (z_ges - h))
     322             :         - 2j * r_plus * r_minus * np.sin(kz * z_diff) * np.exp(1j * kz * h)
     323             :     ) / D(r_plus, r_minus, kz, h)
     324             : 
     325             : 
     326           1 : def Gs(
     327             :     kz: complex,
     328             :     h: float,
     329             :     k_rho: complex,
     330             :     rho: float,
     331             :     phi: float,
     332             :     rs_plus: complex,
     333             :     rs_minus: complex,
     334             :     z_ges: float,
     335             :     z_diff: float,
     336             :     entry: Entries,
     337             : ) -> complex:
     338             :     """Calculate the Gs part of the scattering Green Tensor."""
     339           1 :     if entry in ["xz", "yz", "zx", "zy", "zz"]:
     340           1 :         return 0
     341           1 :     As_plus = A_plus(rs_plus, rs_minus, kz, h, z_ges, z_diff)
     342           1 :     J2 = cached_bessel_function_2(k_rho * rho)
     343           1 :     if entry in ["xy", "yx"]:
     344           1 :         return -As_plus / 2 * J2 * math.sin(2 * phi)
     345           1 :     J0 = cached_bessel_function_0(k_rho * rho)
     346           1 :     if entry == "xx":
     347           1 :         return As_plus / 2 * (J0 + J2 * math.cos(2 * phi))
     348           1 :     if entry == "yy":
     349           1 :         return As_plus / 2 * (J0 - J2 * math.cos(2 * phi))
     350             : 
     351           0 :     raise ValueError(f"Invalid entry '{entry}' for Gs function.")
     352             : 
     353             : 
     354           1 : def Gp(  # noqa: PLR0911
     355             :     kz: complex,
     356             :     h: float,
     357             :     k_rho: complex,
     358             :     rho: float,
     359             :     phi: float,
     360             :     rp_plus: complex,
     361             :     rp_minus: complex,
     362             :     z_ges: float,
     363             :     z_diff: float,
     364             :     entry: Entries,
     365             : ) -> complex:
     366             :     """Calculate the Gp part of the scattering Green Tensor."""
     367           1 :     if entry in ["xz", "zx", "yz", "zy"]:
     368           1 :         J1 = cached_bessel_function_1(k_rho * rho)
     369           1 :         if entry == "zx":
     370           1 :             Bp_minus = B_minus(rp_plus, rp_minus, kz, h, z_ges, z_diff)
     371           1 :             return -1j * (k_rho / kz) * Bp_minus * J1 * math.cos(phi)
     372           1 :         if entry == "xz":
     373           1 :             Bp_plus = B_plus(rp_plus, rp_minus, kz, h, z_ges, z_diff)
     374           1 :             return 1j * (k_rho / kz) * Bp_plus * J1 * math.cos(phi)
     375           1 :         if entry == "yz":
     376           1 :             Bp_plus = B_plus(rp_plus, rp_minus, kz, h, z_ges, z_diff)
     377           1 :             return 1j * (k_rho / kz) * Bp_plus * J1 * math.sin(phi)
     378           1 :         if entry == "zy":
     379           1 :             Bp_minus = B_minus(rp_plus, rp_minus, kz, h, z_ges, z_diff)
     380           1 :             return -1j * (k_rho / kz) * Bp_minus * J1 * math.sin(phi)
     381             : 
     382           1 :     if entry == "zz":
     383           1 :         J0 = cached_bessel_function_0(k_rho * rho)
     384           1 :         Ap_plus = A_plus(rp_plus, rp_minus, kz, h, z_ges, z_diff)
     385           1 :         return -(k_rho**2 / kz**2) * Ap_plus * J0
     386             : 
     387           1 :     J2 = cached_bessel_function_2(k_rho * rho)
     388           1 :     Ap_minus = A_minus(rp_plus, rp_minus, kz, h, z_ges, z_diff)
     389           1 :     if entry in ["xy", "yx"]:
     390           1 :         return Ap_minus / 2 * J2 * math.sin(2 * phi)
     391             : 
     392           1 :     J0 = cached_bessel_function_0(k_rho * rho)
     393           1 :     if entry == "xx":
     394           1 :         return Ap_minus / 2 * (J0 - J2 * math.cos(2 * phi))
     395           1 :     if entry == "yy":
     396           1 :         return Ap_minus / 2 * (J0 + J2 * math.cos(2 * phi))
     397             : 
     398           0 :     raise ValueError(f"Invalid entry '{entry}' for Gp function.")
     399             : 
     400             : 
     401           1 : """ The integrals for the scattering Green Tensor are evaluated in two parts:
     402             :     The first part is along an elliptical path from 0 to 2*k_maj in the complex plane to avoid singularities,
     403             :     and the second part is along the real axis from 2*k_maj to an upper limit.
     404             : 
     405             :     The methods for evaluating these integrals are explained in the papers:
     406             :     - "Accurate and efficient computation of the Green's tensor for stratified media"
     407             :         section III.A (https://doi.org/10.1103/PhysRevE.62.5797)
     408             :     - "Challenges in Computational Electromagnetics: Analysis and Optimization of Planar Multilayered Structures"
     409             :         section 2.4.1 (https://doi.org/10.5075/epfl-thesis-5122)
     410             :     """
     411             : 
     412             : 
     413           1 : @njit(cache=True)
     414           1 : def integrand_ellipse_partial(
     415             :     t: complex,
     416             :     k_maj: float,
     417             :     k_min: float,
     418             :     k0: float,
     419             :     epsilon0: float,
     420             :     epsilon1: complex,
     421             :     epsilon2: complex,
     422             :     h: float,
     423             : ) -> tuple[complex, complex, complex, complex, complex, complex, complex]:
     424             :     # elliptical path, substitution
     425           0 :     k_rho = k_maj * (1 + np.cos(t)) - 1j * k_min * np.sin(t)
     426           0 :     dk_rho = -k_maj * np.sin(t) - 1j * k_min * np.cos(t)
     427             : 
     428             :     # Wave vector components
     429           0 :     kz = branch(epsilon0, k0, k_rho)
     430           0 :     k1z = branch(epsilon1, k0, k_rho)
     431           0 :     k2z = branch(epsilon2, k0, k_rho)
     432             : 
     433           0 :     rs_plus = rs(kz, k1z)
     434           0 :     rs_minus = rs(kz, k2z)
     435           0 :     rp_plus = rp(kz, k1z, epsilon1)
     436           0 :     rp_minus = rp(kz, k2z, epsilon2)
     437             : 
     438           0 :     if k_rho == 0 and dk_rho == 0:  # noqa: SIM108
     439           0 :         prefactor = 0.0
     440             :     else:
     441           0 :         prefactor = 1j / (4 * np.pi) * (k_rho / kz) * np.exp(1j * kz * h) * dk_rho
     442           0 :     return k_rho, kz, rs_plus, rs_minus, rp_plus, rp_minus, prefactor
     443             : 
     444             : 
     445           1 : def integrand_ellipse(
     446             :     t: complex,
     447             :     k_maj: float,
     448             :     k_min: float,
     449             :     k0: float,
     450             :     epsilon0: float,
     451             :     epsilon1: complex,
     452             :     epsilon2: complex,
     453             :     h: float,
     454             :     rho: float,
     455             :     phi: float,
     456             :     z_ges: float,
     457             :     z_diff: float,
     458             :     entry: Entries,
     459             :     real_or_imag: str,
     460             : ) -> complex:
     461           1 :     k_rho, kz, rs_plus, rs_minus, rp_plus, rp_minus, prefactor = integrand_ellipse_partial(
     462             :         t, k_maj, k_min, k0, epsilon0, epsilon1, epsilon2, h
     463             :     )
     464           1 :     if k0 == 0 and kz == 0:
     465           1 :         return 0.0
     466             : 
     467           0 :     gs = Gs(kz, h, k_rho, rho, phi, rs_plus, rs_minus, z_ges, z_diff, entry)
     468           0 :     gp = Gp(kz, h, k_rho, rho, phi, rp_plus, rp_minus, z_ges, z_diff, entry)
     469             : 
     470           0 :     integrand = prefactor * (k0**2 * gs - kz**2 * gp)
     471           0 :     if real_or_imag == "real":
     472           0 :         return np.real(integrand)
     473           0 :     if real_or_imag == "imag":
     474           0 :         return np.imag(integrand)
     475           0 :     raise ValueError("real_or_imag must be 'real' or 'imag'")
     476             : 
     477             : 
     478           1 : def elliptic_integral(
     479             :     omega: float,
     480             :     h: float,
     481             :     rho: float,
     482             :     phi: float,
     483             :     epsilon0: complex,
     484             :     epsilon1: complex,
     485             :     epsilon2: complex,
     486             :     z_ges: float,
     487             :     z_diff: float,
     488             :     entry: Entries,
     489             :     *,
     490             :     only_real_part: bool = False,
     491             : ) -> complex:
     492             :     """Evaluate the elliptic part of the integral.
     493             : 
     494             :     Args:
     495             :         omega: Angular frequency (i.e. 2*pi*f) in 1/s
     496             :         h: Distance between the two surfaces in meters
     497             :         rho: In-plane distance between the two atoms in meters
     498             :         phi: Angle between the in-plane distance vector and the x-axis in radians
     499             :         epsilon0: Electric permittivity of the medium between the two surfaces (dimensionless, complex)
     500             :         epsilon1: Electric permittivity of the upper medium (dimensionless, complex)
     501             :         epsilon2: Electric permittivity of the lower medium (dimensionless, complex)
     502             :         z_ges: Sum of the z-positions of the two atoms in meters
     503             :         z_diff: Difference of the z-positions of the two atoms in meters
     504             :         entry: Entry of the Green tensor to calculate
     505             :         only_real_part: If True, only the real part of the integral is calculated (default: False)
     506             : 
     507             :     Returns: The value of the integral along the elliptical path as a complex number (1/m)
     508             : 
     509             :     """
     510           1 :     k_vac = omega / const.c  # magnitude of wave vector in vacuum
     511           1 :     k0 = k_vac * np.sqrt(epsilon0)
     512             : 
     513             :     # Elliptical path in complex plane to avoid singularities (Integral from 0 to 2k_maj)
     514           1 :     k1 = k_vac * np.sqrt(epsilon1)
     515           1 :     k2 = k_vac * np.sqrt(epsilon2)
     516           1 :     kl_max = max(np.real(k0), np.real(k1), np.real(k2))
     517             : 
     518           1 :     k_maj = (kl_max + k_vac) / 2  # major axis of ellipse
     519           1 :     k_min = min(k_vac, 1 / rho) if rho != 0 else k_vac
     520             : 
     521           1 :     args = (k_maj, k_min, k0, epsilon0, epsilon1, epsilon2, h, rho, phi, z_ges, z_diff, entry)
     522             : 
     523           1 :     real_ellipse, _ = quad(integrand_ellipse, np.pi, 0, args=(*args, "real"), epsrel=1e-9, limit=1000)  # type: ignore [arg-type]
     524           1 :     if only_real_part:
     525           1 :         return real_ellipse
     526           0 :     imag_ellipse, _ = quad(integrand_ellipse, np.pi, 0, args=(*args, "imag"), epsrel=1e-9, limit=1000)  # type: ignore [arg-type]
     527           0 :     return real_ellipse + 1j * imag_ellipse
     528             : 
     529             : 
     530           1 : def integrand_real(
     531             :     k_rho: complex,
     532             :     k0: float,
     533             :     epsilon0: float,
     534             :     epsilon1: complex,
     535             :     epsilon2: complex,
     536             :     h: float,
     537             :     rho: float,
     538             :     phi: float,
     539             :     z_ges: float,
     540             :     z_diff: float,
     541             :     entry: Entries,
     542             :     real_or_imag: str,
     543             : ) -> complex:
     544           1 :     kz = branch(epsilon0, k0, k_rho)
     545           1 :     if kz == 0 and k0 == 0:
     546           0 :         return 0
     547             : 
     548           1 :     k1z = branch(epsilon1, k0, k_rho)
     549           1 :     k2z = branch(epsilon2, k0, k_rho)
     550             : 
     551           1 :     rs_plus = rs(kz, k1z)
     552           1 :     rs_minus = rs(kz, k2z)
     553           1 :     rp_plus = rp(kz, k1z, epsilon1)
     554           1 :     rp_minus = rp(kz, k2z, epsilon2)
     555             : 
     556             :     # Integrand
     557           1 :     integrand = (
     558             :         1j
     559             :         / (4 * np.pi)
     560             :         * (
     561             :             k0**2 * Gs(kz, h, k_rho, rho, phi, rs_plus, rs_minus, z_ges, z_diff, entry)
     562             :             - kz**2 * Gp(kz, h, k_rho, rho, phi, rp_plus, rp_minus, z_ges, z_diff, entry)
     563             :         )
     564             :         * (k_rho / kz)
     565             :         * np.exp(1j * kz * h)
     566             :     )
     567           1 :     if real_or_imag == "real":
     568           1 :         return np.real(integrand)  # type: ignore [no-any-return]
     569           0 :     if real_or_imag == "imag":
     570           0 :         return np.imag(integrand)  # type: ignore [no-any-return]
     571           0 :     raise ValueError("real_or_imag must be 'real' or 'imag'")
     572             : 
     573             : 
     574           1 : def real_axis_integral(
     575             :     omega: float,
     576             :     h: float,
     577             :     rho: float,
     578             :     phi: float,
     579             :     epsilon0: complex,
     580             :     epsilon1: complex,
     581             :     epsilon2: complex,
     582             :     z_ges: float,
     583             :     z_diff: float,
     584             :     entry: Entries,
     585             :     *,
     586             :     only_real_part: bool = False,
     587             : ) -> complex:
     588             :     """Evaluate the real axis part of the integral.
     589             : 
     590             :     Args:
     591             :         omega: Angular frequency (i.e. 2*pi*f) in 1/s
     592             :         h: Distance between the two surfaces in meters
     593             :         rho: In-plane distance between the two atoms in meters
     594             :         phi: Angle between the in-plane distance vector and the x-axis in radians
     595             :         epsilon0: Electric permittivity of the medium between the two surfaces (dimensionless, complex)
     596             :         epsilon1: Electric permittivity of the upper medium (dimensionless, complex)
     597             :         epsilon2: Electric permittivity of the lower medium (dimensionless, complex)
     598             :         z_ges: Sum of the z-positions of the two atoms in meters
     599             :         z_diff: Difference of the z-positions of the two atoms in meters
     600             :         entry: Entry of the Green tensor to calculate
     601             :         upper_limit: Upper limit for the real axis integral (1/m)
     602             :         only_real_part: If True, only the real part of the integral is calculated (default: False)
     603             : 
     604             :     Returns: The value of the integral along the real axis as a complex number (1/m)
     605             : 
     606             :     """
     607           1 :     k_vac = omega / const.c  # magnitude of wave vector in vacuum
     608           1 :     k0 = k_vac * np.sqrt(epsilon0)
     609           1 :     k1 = k_vac * np.sqrt(epsilon1)
     610           1 :     k2 = k_vac * np.sqrt(epsilon2)
     611             : 
     612           1 :     kl_max = max(np.real(k0), np.real(k1), np.real(k2))
     613           1 :     k_maj = (kl_max + k_vac) / 2
     614             : 
     615           1 :     args = (k0, epsilon0, epsilon1, epsilon2, h, rho, phi, z_ges, z_diff, entry)
     616             : 
     617             :     # Estimate the upper limit for the real axis integral
     618           1 :     upper_limit = np.sqrt((745 / h) ** 2 + 1)
     619             : 
     620           1 :     real_real, _ = quad(
     621             :         integrand_real,  # type: ignore [arg-type]
     622             :         2 * k_maj,
     623             :         upper_limit,
     624             :         args=(*args, "real"),  # type: ignore [arg-type]
     625             :         limit=1000,
     626             :         epsrel=1e-9,
     627             :     )
     628           1 :     if only_real_part:
     629           1 :         return real_real
     630           0 :     imag_real, _ = quad(
     631             :         integrand_real,  # type: ignore [arg-type]
     632             :         2 * k_maj,
     633             :         upper_limit,
     634             :         args=(*args, "imag"),  # type: ignore [arg-type]
     635             :         limit=1000,
     636             :         epsrel=1e-9,
     637             :     )
     638           0 :     return real_real + 1j * imag_real

Generated by: LCOV version 1.16