Exemple #1
0
def translation_coefficients_svwf(tau1, l1, m1, tau2, l2, m2, k, d, sph_hankel=None, legendre=None, exp_immphi=None):
    r"""Coefficients of the translation operator for the expansion of an outgoing spherical wave in terms of
    regular spherical waves with respect to a different origin:

    .. math::
        \mathbf{\Psi}_{\tau l m}^{(3)}(\mathbf{r} + \mathbf{d} = \sum_{\tau'} \sum_{l'} \sum_{m'}
        A_{\tau l m, \tau' l' m'} (\mathbf{d}) \mathbf{\Psi}_{\tau' l' m'}^{(1)}(\mathbf{r})

    for :math:`|\mathbf{r}|<|\mathbf{d}|`.
    
    See also section 2.3.3 and appendix B of [Egel 2018 diss].
    
    Args:
        tau1 (int):             tau1=0,1: Original wave's spherical polarization
        l1 (int):               l=1,...: Original wave's SVWF multipole degree
        m1 (int):               m=-l,...,l: Original wave's SVWF multipole order
        tau2 (int):             tau2=0,1: Partial wave's spherical polarization
        l2 (int):               l=1,...: Partial wave's SVWF multipole degree
        m2 (int):               m=-l,...,l: Partial wave's SVWF multipole order
        k (float or complex):   wavenumber (inverse length unit)
        d (list):               translation vectors in format [dx, dy, dz] (length unit)
                                dx, dy, dz can be scalars or ndarrays
        sph_hankel (list):      Optional. sph_hankel[i] contains the spherical hankel funciton of degree i, evaluated at
                                k*d where d is the norm of the distance vector(s)
        legendre (list):        Optional. legendre[l][m] contains the legendre function of order l and degree m,
                                evaluated at cos(theta) where theta is the polar angle(s) of the distance vector(s)

    Returns:
        translation coefficient A (complex)

    """
    # spherical coordinates of d:
    dd = np.sqrt(d[0] ** 2 + d[1] ** 2 + d[2] ** 2)

    if exp_immphi is None:
        phid = np.arctan2(d[1], d[0])
        eimph = np.exp(1j * (m1 - m2) * phid)
    else:
        eimph = exp_immphi[m1][m2]

    if sph_hankel is None:
        sph_hankel = [mathfunc.spherical_hankel(n, k * dd) for n in range(l1 + l2 + 1)]

    if legendre is None:
        costthetd = d[2] / dd
        sinthetd = np.sqrt(d[0] ** 2 + d[1] ** 2) / dd
        legendre, _, _ = mathfunc.legendre_normalized(costthetd, sinthetd, l1 + l2)

    A = complex(0)
    for ld in range(abs(l1 - l2), l1 + l2 + 1):
        a5, b5 = ab5_coefficients(l1, m1, l2, m2, ld)
        if tau1 == tau2:
            A += a5 * sph_hankel[ld] * legendre[ld][abs(m1 - m2)]
        else:
            A += b5 * sph_hankel[ld] * legendre[ld][abs(m1 - m2)]
    A = eimph * A
    return A
Exemple #2
0
def transformation_coefficients_vwf(tau, l, m, pol, kp=None, kz=None, pilm_list=None, taulm_list=None, dagger=False):
    r"""Transformation coefficients B to expand SVWF in PVWF and vice versa:

    .. math::
        B_{\tau l m,j}(x) = -\frac{1}{\mathrm{i}^{l+1}} \frac{1}{\sqrt{2l(l+1)}} (\mathrm{i} \delta_{j1} + \delta_{j2})
        (\delta_{\tau j} \tau_l^{|m|}(x) + (1-\delta_{\tau j} m \pi_l^{|m|}(x))

    For the definition of the :math:`\tau_l^m` and :math:`\pi_l^m` functions, see
    `A. Doicu, T. Wriedt, and Y. A. Eremin: "Light Scattering by Systems of Particles", Springer-Verlag, 2006
    <https://doi.org/10.1007/978-3-540-33697-6>`_
    
    Compare also section 2.3.3 of [Egel 2018 diss].
    
    Args:
        tau (int):          SVWF polarization, 0 for spherical TE, 1 for spherical TM
        l (int):            l=1,... SVWF multipole degree
        m (int):            m=-l,...,l SVWF multipole order
        pol (int):          PVWF polarization, 0 for TE, 1 for TM
        kp (numpy array):         PVWF in-plane wavenumbers
        kz (numpy array):         complex numpy-array: PVWF out-of-plane wavenumbers
        pilm_list (list):   2D list numpy-arrays: alternatively to kp and kz, pilm and taulm as generated with
                            legendre_normalized can directly be handed
        taulm_list (list):  2D list numpy-arrays: alternatively to kp and kz, pilm and taulm as generated with
                            legendre_normalized can directly be handed
        dagger (bool):      switch on when expanding PVWF in SVWF and off when expanding SVWF in PVWF

    Returns:
        Transformation coefficient as array (size like kp).
    """
    if pilm_list is None:
        k = np.sqrt(kp**2 + kz**2)
        ct = kz / k
        st = kp / k
        plm_list, pilm_list, taulm_list = mathfunc.legendre_normalized(ct, st, l)

    if tau == pol:
        sphfun = taulm_list[l][abs(m)]
    else:
        sphfun = m * pilm_list[l][abs(m)]

    if dagger:
        if pol == 0:
            prefac = -1 / (-1j) ** (l + 1) / np.sqrt(2 * l * (l + 1)) * (-1j)
        elif pol == 1:
            prefac = -1 / (-1j) ** (l + 1) / np.sqrt(2 * l * (l + 1)) * 1
        else:
            raise ValueError('pol must be 0 (TE) or 1 (TM)')
    else:
        if pol == 0:
            prefac = -1 / (1j) ** (l + 1) / np.sqrt(2 * l * (l + 1)) * (1j)
        elif pol ==1:
            prefac = -1 / (1j) ** (l + 1) / np.sqrt(2 * l * (l + 1)) * 1
        else:
            raise ValueError('pol must be 0 (TE) or 1 (TM)')

    B = prefac * sphfun
    return B
Exemple #3
0
def pwe_to_swe_conversion(pwe, l_max, m_max, reference_point):
    """Convert plane wave expansion object to a spherical wave expansion object.

    Args:
        pwe (PlaneWaveExpansion):   Plane wave expansion to be converted
        l_max (int):                Maximal multipole degree of spherical wave expansion
        m_max (int):                Maximal multipole order of spherical wave expansion
        reference_point (list):     Coordinates of reference point in the format [x, y, z]

    Returns:
        SphericalWaveExpansion object.
    """

    if reference_point[2] < pwe.lower_z or reference_point[2] > pwe.upper_z:
        raise ValueError('reference point not inside domain of pwe validity')

    swe = fex.SphericalWaveExpansion(k=pwe.k, l_max=l_max, m_max=m_max, kind='regular', 
                                 reference_point=reference_point, lower_z=pwe.lower_z, 
                                 upper_z=pwe.upper_z)
    kpgrid = pwe.k_parallel_grid()
    agrid = pwe.azimuthal_angle_grid()
    kx = kpgrid * np.cos(agrid)
    ky = kpgrid * np.sin(agrid)
    kz = pwe.k_z_grid()
    kzvec = pwe.k_z()

    kvec = np.array([kx, ky, kz])
    rswe_mn_rpwe = np.array(reference_point) - np.array(pwe.reference_point)

    # phase factor for the translation of the reference point from rvec_iS to rvec_S
    ejkriSS = np.exp(1j * (kvec[0] * rswe_mn_rpwe[0] + kvec[1] * rswe_mn_rpwe[1] + kvec[2] * rswe_mn_rpwe[2]))

    # phase factor times pwe coefficients
    gejkriSS = pwe.coefficients * ejkriSS[None, :, :]  # indices: pol, jk, ja
    
    ct = kzvec / pwe.k
    st = pwe.k_parallel / pwe.k
    plm_list, pilm_list, taulm_list = mathfunc.legendre_normalized(ct, st, l_max)

    for m in range(-m_max, m_max + 1):
        emjma_geijkriSS = np.exp(-1j * m * pwe.azimuthal_angles)[None, None, :] * gejkriSS
        for l in range(max(1, abs(m)), l_max + 1):
            for tau in range(2):
                ak_integrand = np.zeros(kpgrid.shape, dtype=complex)
                for pol in range(2):
                    Bdag = transformation_coefficients_vwf(tau, l, m, pol=pol, pilm_list=pilm_list,
                                                               taulm_list=taulm_list, kz=kzvec, dagger=True)
                    ak_integrand += Bdag[:, None] * emjma_geijkriSS[pol, :, :]
                if len(pwe.k_parallel) > 1:
                    an = np.trapz(np.trapz(ak_integrand, pwe.azimuthal_angles) * pwe.k_parallel, pwe.k_parallel) * 4
                else:
                    an = ak_integrand * 4
                swe.coefficients[flds.multi_to_single_index(tau, l, m, swe.l_max, swe.m_max)] = np.squeeze(an)
    return swe
def spherical_vector_wave_function(x, y, z, k, nu, tau, l, m):
    """Electric field components of spherical vector wave function (SVWF). The conventions are chosen according to
    `A. Doicu, T. Wriedt, and Y. A. Eremin: "Light Scattering by Systems of Particles", Springer-Verlag, 2006
    <https://doi.org/10.1007/978-3-540-33697-6>`_
    See also section 2.3.2 of [Egel 2018 diss].

    Args:
        x (numpy.ndarray):      x-coordinate of position where to test the field (length unit)
        y (numpy.ndarray):      y-coordinate of position where to test the field
        z (numpy.ndarray):      z-coordinate of position where to test the field
        k (float or complex):   wavenumber (inverse length unit)
        nu (int):               1 for regular waves, 3 for outgoing waves
        tau (int):              spherical polarization, 0 for spherical TE and 1 for spherical TM
        l (int):                l=1,... multipole degree (polar quantum number)
        m (int):                m=-l,...,l multipole order (azimuthal quantum number)

    Returns:
        - x-coordinate of SVWF electric field (numpy.ndarray)
        - y-coordinate of SVWF electric field (numpy.ndarray)
        - z-coordinate of SVWF electric field (numpy.ndarray)
    """
    x = np.array(x)
    y = np.array(y)
    z = np.array(z)

    r = np.sqrt(x**2 + y**2 + z**2)
    non0 = np.logical_not(r == 0)

    theta = np.zeros(x.shape)
    theta[non0] = np.arccos(z[non0] / r[non0])
    phi = np.arctan2(y, x)

    # unit vector in r-direction

    er_x = np.zeros(x.shape)
    er_y = np.zeros(x.shape)
    er_z = np.ones(x.shape)

    er_x[non0] = x[non0] / r[non0]
    er_y[non0] = y[non0] / r[non0]
    er_z[non0] = z[non0] / r[non0]

    # unit vector in theta-direction
    eth_x = np.cos(theta) * np.cos(phi)
    eth_y = np.cos(theta) * np.sin(phi)
    eth_z = -np.sin(theta)

    # unit vector in phi-direction
    eph_x = -np.sin(phi)
    eph_y = np.cos(phi)
    eph_z = x - x

    cos_thet = np.cos(theta)
    sin_thet = np.sin(theta)
    plm_list, pilm_list, taulm_list = sf.legendre_normalized(
        cos_thet, sin_thet, l)
    plm = plm_list[l][abs(m)]
    pilm = pilm_list[l][abs(m)]
    taulm = taulm_list[l][abs(m)]

    kr = k * r
    if nu == 1:
        bes = sf.spherical_bessel(l, kr)
        dxxz = sf.dx_xj(l, kr)
        bes_kr = np.zeros(kr.shape, dtype=np.complex)
        dxxz_kr = np.zeros(kr.shape, dtype=np.complex)
        zero = (r == 0)
        nonzero = np.logical_not(zero)
        bes_kr[zero] = (l == 1) / 3
        dxxz_kr[zero] = (l == 1) * 2 / 3
        bes_kr[nonzero] = (bes[nonzero] / kr[nonzero])
        dxxz_kr[nonzero] = (dxxz[nonzero] / kr[nonzero])
    elif nu == 3:
        bes = sf.spherical_hankel(l, kr)
        dxxz = sf.dx_xh(l, kr)
        bes_kr = bes / kr
        dxxz_kr = dxxz / kr
    else:
        raise ValueError('nu must be 1 (regular SVWF) or 3 (outgoing SVWF)')

    eimphi = np.exp(1j * m * phi)
    prefac = 1 / np.sqrt(2 * l * (l + 1))
    if tau == 0:
        Ex = prefac * bes * (1j * m * pilm * eth_x - taulm * eph_x) * eimphi
        Ey = prefac * bes * (1j * m * pilm * eth_y - taulm * eph_y) * eimphi
        Ez = prefac * bes * (1j * m * pilm * eth_z - taulm * eph_z) * eimphi
    elif tau == 1:
        Ex = prefac * (l * (l + 1) * bes_kr * plm * er_x + dxxz_kr *
                       (taulm * eth_x + 1j * m * pilm * eph_x)) * eimphi
        Ey = prefac * (l * (l + 1) * bes_kr * plm * er_y + dxxz_kr *
                       (taulm * eth_y + 1j * m * pilm * eph_y)) * eimphi
        Ez = prefac * (l * (l + 1) * bes_kr * plm * er_z + dxxz_kr *
                       (taulm * eth_z + 1j * m * pilm * eph_z)) * eimphi
    else:
        raise ValueError('tau must be 0 (spherical TE) or 1 (spherical TM)')

    return Ex, Ey, Ez
def compute_J_cyl(lmax, Ntheta, a, h, n0, ns, wavelength, nu):
    #dimension of final T-matrix is 2*nmax x 2*nmax for each individual matrix
    nmax = int(lmax * (lmax + 2))

    #preallocate space for both J and dJ matrices of size nmax x nmax for J matrices
    #and dJ matrices are nmax x nmax x 2
    #dJ[:,:,0] is dJ/da
    #dJ[:,:,1] is dJ/dh
    J11 = np.zeros((nmax, nmax), dtype=np.complex_)
    J12 = np.zeros((nmax, nmax), dtype=np.complex_)
    J21 = np.zeros((nmax, nmax), dtype=np.complex_)
    J22 = np.zeros((nmax, nmax), dtype=np.complex_)

    dJ11 = np.zeros((nmax, nmax, 2), dtype=np.complex_)
    dJ12 = np.zeros((nmax, nmax, 2), dtype=np.complex_)
    dJ21 = np.zeros((nmax, nmax, 2), dtype=np.complex_)
    dJ22 = np.zeros((nmax, nmax, 2), dtype=np.complex_)

    #find the angle theta at which the corner of the cylinder is at
    theta_edge = np.arctan(2 * a / h)
    #prepare gauss-legendre quadrature for interval of [-1,1] to perform numerical integral
    [x_norm, wt_norm] = legendre.leggauss(Ntheta)

    #rescale integration points and weights to match actual bounds:
    #   circ covers the circular surface of the cylinder (end caps)
    #   body covers the rectangular surface of the cylinder (body area)
    #circ integral goes from 0 to theta_edge, b = theta_edge, a = 0
    theta_circ = theta_edge / 2 * x_norm + theta_edge / 2
    wt_circ = theta_edge / 2 * wt_norm

    #body integral goes from theta_edge to pi/2, b = pi/2, a = theta_edge
    theta_body = (np.pi / 2 - theta_edge) / 2 * x_norm + (np.pi / 2 +
                                                          theta_edge) / 2
    wt_body = (np.pi / 2 - theta_edge) / 2 * wt_norm

    #merge the circ and body lists into a single map
    theta_map = np.concatenate((theta_circ, theta_body), axis=0)
    weight_map = np.concatenate((wt_circ, wt_body), axis=0)

    #identify indices corresponding to the circular end caps and rectangular body
    circ_idx = np.arange(0, Ntheta - 1)
    body_idx = np.arange(Ntheta - 1, 2 * Ntheta)

    #k vectors of the light in medium (ki) and in scatterer (ks)
    ki = 2 * np.pi * n0 / wavelength
    ks = 2 * np.pi * ns / wavelength

    #precompute trig functions
    ct = np.cos(theta_map)
    st = np.sin(theta_map)
    #normal vector for circular surface (circ) requires tangent
    tant = np.tan(theta_map[circ_idx])
    #normal vector for rectangular surface (body) requires cotangent
    cott = 1 / np.tan(theta_map[body_idx])

    #precompute spherical angular polynomials
    [p_lm, pi_lm, tau_lm] = sf.legendre_normalized(ct, st, lmax)

    #radial coordinate of the surface, and the derivatives with respect to a and h
    #r_c: radial coordinate of circular end cap
    #r_b: radial coordinate of rectangular body
    r_c = h / 2 / ct[circ_idx]
    dr_c = r_c / h
    r_b = a / st[body_idx]
    dr_b = r_b / a

    #merge radial coordiantes into a single vector
    r = np.concatenate((r_c, r_b), axis=0)

    #derivatives of the integration limits for performing derivatives
    da_edge = 2 * h / (h**2 + 4 * a**2)
    dh_edge = -2 * a / (h**2 + 4 * a**2)

    Ntheta = Ntheta - 1

    #loop through each individual element of the J11, J12, J21, J22 matrices
    for li in np.arange(1, lmax + 1):

        #precompute bessel functiosn and derivatives
        b_li = bf.sph_bessel(nu, li, ki * r)
        db_li = bf.d1Z_Z_sph_bessel(nu, li, ki * r)
        db2_li = bf.d2Z_Z_sph_bessel(nu, li, ki * r)
        d1b_li = bf.d1Z_sph_bessel(nu, li, ki * r)
        for lp in np.arange(1, lmax + 1):

            #precompute bessel functions and derivatives
            j_lp = bf.sph_bessel(1, lp, ks * r)
            dj_lp = bf.d1Z_Z_sph_bessel(1, lp, ks * r)
            dj2_lp = bf.d2Z_Z_sph_bessel(1, lp, ks * r)
            d1j_lp = bf.d1Z_sph_bessel(1, lp, ks * r)

            #compute normalization factor
            lfactor = 1 / np.sqrt(li * (li + 1) * lp * (lp + 1))

            for mi in np.arange(-li, li + 1):

                #compute row index where element is placed
                n_i = compute_n(lmax, 1, li, mi) - 1

                #precompute spherical harmonic functions
                p_limi = p_lm[li][abs(mi)]
                pi_limi = pi_lm[li][abs(mi)]
                tau_limi = tau_lm[li][abs(mi)]

                for mp in np.arange(-lp, lp + 1):

                    #compute col index where element is placed
                    n_p = compute_n(lmax, 1, lp, mp) - 1

                    #precompute spherical harmonic functions
                    p_lpmp = p_lm[lp][abs(mp)]
                    pi_lpmp = pi_lm[lp][abs(mp)]
                    tau_lpmp = tau_lm[lp][abs(mp)]

                    #compute selection rules that includes symmetries
                    sr_1122 = selection_rules(li, mi, lp, mp, 1)
                    sr_1221 = selection_rules(li, mi, lp, mp, 2)

                    #perform integral about phi analytically. This is roughly a sinc function
                    if mi == mp:
                        phi_exp = np.pi
                    else:
                        phi_exp = -1j * (np.exp(1j * (mp - mi) * np.pi) -
                                         1) / (mp - mi)

                    #for J11 and J22 integrals
                    if sr_1122 != 0:
                        prefactor = sr_1122 * lfactor * phi_exp
                        ang = mp * pi_lpmp * tau_limi + mi * pi_limi * tau_lpmp
                        #J11 computation
                        J11_r = -1j * weight_map * prefactor * r**2 * st * j_lp * b_li * ang
                        J11[n_i, n_p] = np.sum(J11_r)
                        #dJ11 computation
                        dJ11dr = 2 * r * j_lp * b_li + r**2 * (
                            ks * d1j_lp * b_li + ki * d1b_li * j_lp)
                        #dJ11/da
                        dJ11[n_i, n_p, 0] = np.sum(
                            -1j * prefactor * weight_map[body_idx] *
                            st[body_idx] * dJ11dr[body_idx] * ang[body_idx] *
                            dr_b)
                        #dJ11/dh
                        dJ11[n_i, n_p, 1] = np.sum(
                            -1j * prefactor * weight_map[circ_idx] *
                            st[circ_idx] * dJ11dr[circ_idx] * ang[circ_idx] *
                            dr_c)
                        #J22 computation split into radial and polar integrals
                        J22_r = -1j * prefactor * weight_map * st / ki / ks * dj_lp * db_li * ang
                        J22_db = lp * (lp + 1) * mi * pi_limi * p_lpmp
                        J22_dj = li * (li + 1) * mp * pi_lpmp * p_limi
                        J22_t = -1j * prefactor * weight_map * st / ki / ks * (
                            J22_db * j_lp * db_li + J22_dj * b_li * dj_lp)
                        J22[n_i, n_p] = np.sum(J22_r) + np.sum(
                            J22_t[circ_idx] * tant) + np.sum(
                                J22_t[body_idx] * -cott)
                        #derivative of boundary term
                        dJ22edge = -1j / ki / ks * st[Ntheta] * (
                            J22_db[Ntheta] * j_lp[Ntheta] * db_li[Ntheta] +
                            J22_dj[Ntheta] * dj_lp[Ntheta] * b_li[Ntheta]
                        ) * (st[Ntheta] / ct[Ntheta] + ct[Ntheta] / st[Ntheta])
                        #dJ22_r/da
                        dJ22da1 = -1j / ki / ks * (
                            ks * dj2_lp[body_idx] * db_li[body_idx] +
                            ki * db2_li[body_idx] * dj_lp[body_idx]
                        ) * dr_b * st[body_idx] * ang[body_idx]
                        #dJ22_t/da
                        dJ22da2 = 1j / ki / ks * cott * st[body_idx] * dr_b * (
                            J22_db[body_idx] *
                            (ks * d1j_lp[body_idx] * db_li[body_idx] +
                             ki * j_lp[body_idx] * db2_li[body_idx]) +
                            J22_dj[body_idx] *
                            (ki * d1b_li[body_idx] * dj_lp[body_idx] +
                             ks * dj2_lp[body_idx] * b_li[body_idx]))
                        #dJ22_r/dh
                        dJ22dh1 = -1j / ki / ks * (
                            ks * dj2_lp[circ_idx] * db_li[circ_idx] +
                            ki * db2_li[circ_idx] * dj_lp[circ_idx]
                        ) * dr_c * st[circ_idx] * ang[circ_idx]
                        #dJ22_t/dh
                        dJ22dh2 = -1j / ki / ks * tant * st[circ_idx] * dr_c * (
                            J22_db[circ_idx] *
                            (ks * d1j_lp[circ_idx] * db_li[circ_idx] +
                             ki * j_lp[circ_idx] * db2_li[circ_idx]) +
                            J22_dj[circ_idx] *
                            (ki * d1b_li[circ_idx] * dj_lp[circ_idx] +
                             ks * dj2_lp[circ_idx] * b_li[circ_idx]))
                        dJ22[n_i, n_p, 0] = np.sum(
                            prefactor * weight_map[body_idx] *
                            dJ22da1) + np.sum(
                                prefactor * weight_map[body_idx] *
                                dJ22da2) + prefactor * dJ22edge * da_edge
                        dJ22[n_i, n_p, 1] = np.sum(
                            prefactor * weight_map[circ_idx] *
                            dJ22dh1) + np.sum(
                                prefactor * weight_map[circ_idx] *
                                dJ22dh2) + prefactor * dJ22edge * dh_edge
                    #for J12 and J21 integrals
                    if sr_1221 != 0:
                        prefactor = sr_1221 * lfactor * phi_exp
                        ang = mi * mp * pi_limi * pi_lpmp + tau_limi * tau_lpmp
                        #J12 computation split into radial and polar
                        J12_r = prefactor * weight_map / ki * r * st * j_lp * db_li * ang
                        J12_t = prefactor * weight_map / ki * r * st * li * (
                            li + 1) * j_lp * b_li * p_limi * tau_lpmp
                        J12[n_i, n_p] = np.sum(J12_r) + np.sum(
                            J12_t[circ_idx] * tant) + np.sum(
                                J12_t[body_idx] * -cott)
                        #derivative of boundary term
                        dJ12edge = li * (
                            li + 1
                        ) / ki * r[Ntheta] * st[Ntheta] * j_lp[Ntheta] * b_li[
                            Ntheta] * tau_lpmp[Ntheta] * p_limi[Ntheta] * (
                                st[Ntheta] / ct[Ntheta] +
                                ct[Ntheta] / st[Ntheta])
                        #dJ12_r/da
                        dJ12da1 = dr_b / ki * (
                            j_lp[body_idx] * db_li[body_idx] + r_b *
                            (ks * d1j_lp[body_idx] * db_li[body_idx] +
                             ki * j_lp[body_idx] * db2_li[body_idx])
                        ) * st[body_idx] * ang[body_idx]
                        #dJ12_t/da
                        dJ12da2 = -li * (li + 1) / ki * dr_b * (
                            j_lp[body_idx] * b_li[body_idx] + r_b *
                            (ks * d1j_lp[body_idx] * b_li[body_idx] +
                             ki * j_lp[body_idx] * d1b_li[body_idx])
                        ) * cott * st[body_idx] * tau_lpmp[body_idx] * p_limi[
                            body_idx]
                        #dJ12_r/dh
                        dJ12dh1 = dr_c / ki * (
                            j_lp[circ_idx] * db_li[circ_idx] + r_c *
                            (ks * d1j_lp[circ_idx] * db_li[circ_idx] +
                             ki * j_lp[circ_idx] * db2_li[circ_idx])
                        ) * st[circ_idx] * ang[circ_idx]
                        #dJ12_t/dh
                        dJ12dh2 = li * (li + 1) / ki * dr_c * (
                            j_lp[circ_idx] * b_li[circ_idx] + r_c *
                            (ks * d1j_lp[circ_idx] * b_li[circ_idx] +
                             ki * j_lp[circ_idx] * d1b_li[circ_idx])
                        ) * tant * st[circ_idx] * tau_lpmp[circ_idx] * p_limi[
                            circ_idx]
                        dJ12[n_i, n_p, 0] = np.sum(
                            prefactor * weight_map[body_idx] *
                            dJ12da1) + np.sum(
                                prefactor * weight_map[body_idx] *
                                dJ12da2) + prefactor * dJ12edge * da_edge
                        dJ12[n_i, n_p, 1] = np.sum(
                            prefactor * weight_map[circ_idx] *
                            dJ12dh1) + np.sum(
                                prefactor * weight_map[circ_idx] *
                                dJ12dh2) + prefactor * dJ12edge * dh_edge
                        #J21 computation split into radial and polar
                        J21_r = -prefactor * weight_map / ks * r * st * dj_lp * b_li * ang
                        J21_t = -prefactor * weight_map / ks * r * st * lp * (
                            lp + 1) * j_lp * b_li * p_lpmp * tau_limi
                        J21[n_i, n_p] = np.sum(J21_r) + np.sum(
                            J21_t[circ_idx] * tant) + np.sum(
                                J21_t[body_idx] * -cott)
                        #derivative of boundary terms
                        dJ21edge = -lp * (
                            lp + 1
                        ) / ks * r[Ntheta] * st[Ntheta] * j_lp[Ntheta] * b_li[
                            Ntheta] * tau_limi[Ntheta] * p_lpmp[Ntheta] * (
                                st[Ntheta] / ct[Ntheta] +
                                ct[Ntheta] / st[Ntheta])
                        #dJ21_r/da
                        dJ21da1 = -dr_b / ks * (
                            b_li[body_idx] * dj_lp[body_idx] + r_b *
                            (ki * d1b_li[body_idx] * dj_lp[body_idx] +
                             ks * dj2_lp[body_idx] * b_li[body_idx])
                        ) * st[body_idx] * ang[body_idx]
                        #dJ21_t/da
                        dJ21da2 = lp * (lp + 1) / ks * dr_b * (
                            j_lp[body_idx] * b_li[body_idx] + r_b *
                            (ks * d1j_lp[body_idx] * b_li[body_idx] +
                             ki * d1b_li[body_idx] * j_lp[body_idx])
                        ) * cott * st[body_idx] * tau_limi[body_idx] * p_lpmp[
                            body_idx]
                        #dJ21_r/dh
                        dJ21dh1 = -dr_c / ks * (
                            b_li[circ_idx] * dj_lp[circ_idx] + r_c *
                            (ki * d1b_li[circ_idx] * dj_lp[circ_idx] +
                             ks * dj2_lp[circ_idx] * b_li[circ_idx])
                        ) * st[circ_idx] * ang[circ_idx]
                        #dJ21_t/dh
                        if n_i == 10:
                            abc = 2
                        dJ21dh2 = -lp * (lp + 1) / ks * dr_c * (
                            j_lp[circ_idx] * b_li[circ_idx] + r_c *
                            (ks * d1j_lp[circ_idx] * b_li[circ_idx] +
                             ki * d1b_li[circ_idx] * j_lp[circ_idx])
                        ) * tant * st[circ_idx] * tau_limi[circ_idx] * p_lpmp[
                            circ_idx]
                        dJ21[n_i, n_p, 0] = np.sum(
                            prefactor * weight_map[body_idx] *
                            dJ21da1) + np.sum(
                                prefactor * weight_map[body_idx] *
                                dJ21da2) + prefactor * dJ21edge * da_edge
                        dJ21[n_i, n_p, 1] = np.sum(
                            prefactor * weight_map[circ_idx] *
                            dJ21dh1) + np.sum(
                                prefactor * weight_map[circ_idx] *
                                dJ21dh2) + prefactor * dJ21edge * dh_edge

    return J11, J12, J21, J22, dJ11, dJ12, dJ21, dJ22
Exemple #6
0
def radial_coupling_lookup_table(vacuum_wavelength,
                                 particle_list,
                                 layer_system,
                                 k_parallel='default',
                                 resolution=None):
    """Prepare Sommerfeld integral lookup table to allow for a fast calculation of the coupling matrix by interpolation.
    This function is called when all particles are on the same z-position.
    
    Args:
        vacuum_wavelength (float):  Vacuum wavelength in length units
        particle_list (list):       List of particle objects
        layer_system (smuthi.layers.LayerSystem):    Stratified medium
        k_parallel (numpy.ndarray or str):           In-plane wavenumber for Sommerfeld integrals.
                                                     If 'default', smuthi.fields.default_Sommerfeld_k_parallel_array
        resolution (float): Spatial resolution of lookup table in length units. (default: vacuum_wavelength / 100)       
                            Smaller means more accurate but higher memory footprint 
                            
    Returns:
        (tuple) tuple containing:
        
            lookup_table (ndarray):  Coupling lookup, indices are [rho, n1, n2].
            rho_array (ndarray):     Values for the radial distance considered for the lookup (starting from negative 
                                     numbers to allow for simpler cubic interpolation without distinction of cases 
                                     at rho=0)
    """

    sys.stdout.write('Prepare radial particle coupling lookup:\n')
    sys.stdout.flush()

    if resolution is None:
        resolution = vacuum_wavelength / 100
        sys.stdout.write('Setting lookup resolution to %f\n' % resolution)
        sys.stdout.flush()

    l_max = max([particle.l_max for particle in particle_list])
    m_max = max([particle.m_max for particle in particle_list])
    blocksize = smuthi.fields.blocksize(l_max, m_max)

    x_array = np.array([particle.position[0] for particle in particle_list])
    y_array = np.array([particle.position[1] for particle in particle_list])
    rho_array = np.sqrt((x_array[:, None] - x_array[None, :])**2 +
                        (y_array[:, None] - y_array[None, :])**2)

    radial_distance_array = np.arange(-3 * resolution,
                                      rho_array.max() + 3 * resolution,
                                      resolution)

    z = particle_list[0].position[2]
    i_s = layer_system.layer_number(z)
    k_is = layer_system.wavenumber(i_s, vacuum_wavelength)
    dz = z - layer_system.reference_z(i_s)

    len_rho = len(radial_distance_array)

    # direct -----------------------------------------------------------------------------------------------------------
    w = np.zeros((len_rho, blocksize, blocksize), dtype=np.complex64)
    sys.stdout.write('Memory footprint: ' + size_format(w.nbytes) + '\n')
    sys.stdout.flush()

    ct = np.array([0.0])
    st = np.array([1.0])
    bessel_h = []
    for n in range(2 * l_max + 1):
        bessel_h.append(sf.spherical_hankel(n, k_is * radial_distance_array))
        bessel_h[-1][radial_distance_array <= 0] = np.nan

    legendre, _, _ = sf.legendre_normalized(ct, st, 2 * l_max)

    pbar = tqdm(
        total=blocksize**2,
        desc='Direct coupling           ',
        file=sys.stdout,
        bar_format='{l_bar}{bar}| elapsed: {elapsed} remaining: {remaining}')

    for m1 in range(-m_max, m_max + 1):
        for m2 in range(-m_max, m_max + 1):
            for l1 in range(max(1, abs(m1)), l_max + 1):
                for l2 in range(max(1, abs(m2)), l_max + 1):
                    A = np.zeros(len_rho, dtype=complex)
                    B = np.zeros(len_rho, dtype=complex)
                    for ld in range(max(abs(l1 - l2), abs(m1 - m2)),
                                    l1 + l2 + 1):  # if ld<abs(m1-m2) then P=0
                        a5, b5 = trf.ab5_coefficients(l2, m2, l1, m1, ld)
                        A = A + a5 * bessel_h[ld] * legendre[ld][abs(m1 - m2)]
                        B = B + b5 * bessel_h[ld] * legendre[ld][abs(m1 - m2)]
                    for tau1 in range(2):
                        n1 = smuthi.fields.multi_to_single_index(
                            tau1, l1, m1, l_max, m_max)
                        for tau2 in range(2):
                            n2 = smuthi.fields.multi_to_single_index(
                                tau2, l2, m2, l_max, m_max)
                            if tau1 == tau2:
                                w[:, n1, n2] = A  # remember that w = A.T
                            else:
                                w[:, n1, n2] = B
                            pbar.update()
    pbar.close()
    close_to_zero = radial_distance_array < rho_array[
        ~np.eye(rho_array.shape[0], dtype=bool)].min() / 2
    w[close_to_zero, :, :] = 0  # switch off direct coupling contribution near rho=0

    # layer mediated ---------------------------------------------------------------------------------------------------
    sys.stdout.write('Layer mediated coupling   : ...')
    sys.stdout.flush()

    if type(k_parallel) == str and k_parallel == 'default':
        k_parallel = smuthi.fields.default_Sommerfeld_k_parallel_array

    kz_is = smuthi.fields.k_z(k_parallel=k_parallel, k=k_is)
    len_kp = len(k_parallel)

    # phase factors
    epl2jkz = np.exp(2j * kz_is * dz)
    emn2jkz = np.exp(-2j * kz_is * dz)

    # layer response
    L = np.zeros((2, 2, 2, len_kp), dtype=complex)  # pol, pl/mn1, pl/mn2, kp
    for pol in range(2):
        L[pol, :, :, :] = lay.layersystem_response_matrix(
            pol, layer_system.thicknesses,
            layer_system.refractive_indices, k_parallel,
            smuthi.fields.angular_frequency(vacuum_wavelength), i_s, i_s)

    # transformation coefficients
    B_dag = np.zeros((2, 2, blocksize, len_kp),
                     dtype=complex)  # pol, pl/mn, n, kp
    B = np.zeros((2, 2, blocksize, len_kp), dtype=complex)  # pol, pl/mn, n, kp
    ct = kz_is / k_is
    st = k_parallel / k_is
    _, pilm_pl, taulm_pl = sf.legendre_normalized(ct, st, l_max)
    _, pilm_mn, taulm_mn = sf.legendre_normalized(-ct, st, l_max)

    m_list = [None for n in range(blocksize)]
    for tau in range(2):
        for m in range(-m_max, m_max + 1):
            for l in range(max(1, abs(m)), l_max + 1):
                n = smuthi.fields.multi_to_single_index(
                    tau, l, m, l_max, m_max)
                m_list[n] = m
                for pol in range(2):
                    B_dag[pol, 0, n, :] = trf.transformation_coefficients_vwf(
                        tau,
                        l,
                        m,
                        pol,
                        pilm_list=pilm_pl,
                        taulm_list=taulm_pl,
                        dagger=True)
                    B_dag[pol, 1, n, :] = trf.transformation_coefficients_vwf(
                        tau,
                        l,
                        m,
                        pol,
                        pilm_list=pilm_mn,
                        taulm_list=taulm_mn,
                        dagger=True)
                    B[pol, 0, n, :] = trf.transformation_coefficients_vwf(
                        tau,
                        l,
                        m,
                        pol,
                        pilm_list=pilm_pl,
                        taulm_list=taulm_pl,
                        dagger=False)
                    B[pol, 1, n, :] = trf.transformation_coefficients_vwf(
                        tau,
                        l,
                        m,
                        pol,
                        pilm_list=pilm_mn,
                        taulm_list=taulm_mn,
                        dagger=False)

    # pairs of (n1, n2), listed by abs(m1-m2)
    n1n2_combinations = [[] for dm in range(2 * m_max + 1)]
    for n1 in range(blocksize):
        m1 = m_list[n1]
        for n2 in range(blocksize):
            m2 = m_list[n2]
            n1n2_combinations[abs(m1 - m2)].append((n1, n2))

    wr = np.zeros((len_rho, blocksize, blocksize), dtype=complex)

    dkp = np.diff(k_parallel)
    if cu.use_gpu:
        re_dkp_d = cu.gpuarray.to_gpu(np.float32(dkp.real))
        im_dkp_d = cu.gpuarray.to_gpu(np.float32(dkp.imag))
        kernel_source_code = cusrc.radial_lookup_assembly_code % (
            blocksize, len_rho, len_kp)
        helper_function = cu.SourceModule(kernel_source_code).get_function(
            "helper")
        cuda_blocksize = 128
        cuda_gridsize = (len_rho + cuda_blocksize - 1) // cuda_blocksize

        re_dwr_d = cu.gpuarray.to_gpu(np.zeros(len_rho, dtype=np.float32))
        im_dwr_d = cu.gpuarray.to_gpu(np.zeros(len_rho, dtype=np.float32))
    n1n2_combinations = [[] for dm in range(2 * m_max + 1)]
    for n1 in range(blocksize):
        m1 = m_list[n1]
        for n2 in range(blocksize):
            m2 = m_list[n2]
            n1n2_combinations[abs(m1 - m2)].append((n1, n2))

    pbar = tqdm(
        total=blocksize**2,
        desc='Layer mediated coupling   ',
        file=sys.stdout,
        bar_format='{l_bar}{bar}| elapsed: {elapsed} remaining: {remaining}')

    for dm in range(2 * m_max + 1):
        bessel = scipy.special.jv(
            dm, (k_parallel[None, :] * radial_distance_array[:, None]))
        besjac = bessel * (k_parallel / (kz_is * k_is))[None, :]
        for n1n2 in n1n2_combinations[dm]:
            n1 = n1n2[0]
            m1 = m_list[n1]
            n2 = n1n2[1]
            m2 = m_list[n2]

            belbe = np.zeros(len_kp, dtype=complex)  # n1, n2, kp
            for pol in range(2):
                belbe += L[pol, 0, 0, :] * B_dag[pol, 0, n1, :] * B[pol, 0,
                                                                    n2, :]
                belbe += L[pol, 1, 0, :] * B_dag[pol, 1,
                                                 n1, :] * B[pol, 0,
                                                            n2, :] * emn2jkz
                belbe += L[pol, 0, 1, :] * B_dag[pol, 0,
                                                 n1, :] * B[pol, 1,
                                                            n2, :] * epl2jkz
                belbe += L[pol, 1, 1, :] * B_dag[pol, 1, n1, :] * B[pol, 1,
                                                                    n2, :]

            if cu.use_gpu:
                re_belbe_d = cu.gpuarray.to_gpu(np.float32(
                    belbe[None, :].real))
                im_belbe_d = cu.gpuarray.to_gpu(np.float32(
                    belbe[None, :].imag))

                re_besjac_d = cu.gpuarray.to_gpu(np.float32(besjac.real))
                im_besjac_d = cu.gpuarray.to_gpu(np.float32(besjac.imag))
                helper_function(re_besjac_d.gpudata,
                                im_besjac_d.gpudata,
                                re_belbe_d.gpudata,
                                im_belbe_d.gpudata,
                                re_dkp_d.gpudata,
                                im_dkp_d.gpudata,
                                re_dwr_d.gpudata,
                                im_dwr_d.gpudata,
                                block=(cuda_blocksize, 1, 1),
                                grid=(cuda_gridsize, 1))

                wr[:, n1, n2] = 4 * (1j)**abs(m2 - m1) * (re_dwr_d.get() +
                                                          1j * im_dwr_d.get())

            else:
                integrand = besjac * belbe[None, :]  # rho, kp
                wr[:, n1, n2] = 2 * (1j)**abs(m2 - m1) * (
                    (integrand[:, :-1] + integrand[:, 1:]) * dkp[None, :]).sum(
                        axis=-1)  # trapezoidal rule
            pbar.update()
    pbar.close()

    return w + wr, radial_distance_array
Exemple #7
0
def volumetric_coupling_lookup_table(vacuum_wavelength,
                                     particle_list,
                                     layer_system,
                                     k_parallel='default',
                                     resolution=None):
    """Prepare Sommerfeld integral lookup table to allow for a fast calculation of the coupling matrix by interpolation.
    This function is called when not all particles are on the same z-position.
    
    Args:
        vacuum_wavelength (float):  Vacuum wavelength in length units
        particle_list (list):       List of particle objects
        layer_system (smuthi.layers.LayerSystem):    Stratified medium
        k_parallel (numpy.ndarray or str):           In-plane wavenumber for Sommerfeld integrals.
                                                     If 'default', smuthi.fields.default_Sommerfeld_k_parallel_array
        resolution (float): Spatial resolution of lookup table in length units. (default: vacuum_wavelength / 100)        
                            Smaller means more accurate but higher memory footprint 
                            
    Returns:
        (tuple): tuple containing:

            w_pl (ndarray):  Coupling lookup for z1 + z2, indices are [rho, z, n1, n2]. Includes layer mediated coupling.
            w_mn (ndarray):  Coupling lookup for z1 + z2, indices are [rho, z, n1, n2]. Includes layer mediated and 
                             direct coupling.
            rho_array (ndarray):    Values for the radial distance considered for the lookup (starting from negative 
                                    numbers to allow for simpler cubic interpolation without distinction of cases 
                                    for lookup edges
            sz_array (ndarray):     Values for the sum of z-coordinates (z1 + z2) considered for the lookup
            dz_array (ndarray):     Values for the difference of z-coordinates (z1 - z2) considered for the lookup 
    """
    sys.stdout.write('Prepare 3D particle coupling lookup:\n')
    sys.stdout.flush()

    if resolution is None:
        resolution = vacuum_wavelength / 100
        sys.stdout.write('Setting lookup resolution to %f\n' % resolution)
        sys.stdout.flush()

    l_max = max([particle.l_max for particle in particle_list])
    m_max = max([particle.m_max for particle in particle_list])
    blocksize = smuthi.fields.blocksize(l_max, m_max)

    particle_x_array = np.array(
        [particle.position[0] for particle in particle_list])
    particle_y_array = np.array(
        [particle.position[1] for particle in particle_list])
    particle_z_array = np.array(
        [particle.position[2] for particle in particle_list])
    particle_rho_array = np.sqrt(
        (particle_x_array[:, None] - particle_x_array[None, :])**2 +
        (particle_y_array[:, None] - particle_y_array[None, :])**2)

    dz_min = particle_z_array.min() - particle_z_array.max()
    dz_max = particle_z_array.max() - particle_z_array.min()
    sz_min = 2 * particle_z_array.min()
    sz_max = 2 * particle_z_array.max()

    rho_array = np.arange(-3 * resolution,
                          particle_rho_array.max() + 3 * resolution,
                          resolution)
    sz_array = np.arange(sz_min - 3 * resolution, sz_max + 3 * resolution,
                         resolution)
    dz_array = np.arange(dz_min - 3 * resolution, dz_max + 3 * resolution,
                         resolution)

    len_rho = len(rho_array)
    len_sz = len(sz_array)
    len_dz = len(dz_array)
    assert len_sz == len_dz

    i_s = layer_system.layer_number(particle_list[0].position[2])
    k_is = layer_system.wavenumber(i_s, vacuum_wavelength)
    z_is = layer_system.reference_z(i_s)

    # direct -----------------------------------------------------------------------------------------------------------
    w = np.zeros((len_rho, len_dz, blocksize, blocksize), dtype=np.complex64)
    sys.stdout.write('Lookup table memory footprint: ' +
                     size_format(2 * w.nbytes) + '\n')
    sys.stdout.flush()

    r_array = np.sqrt(dz_array[None, :]**2 + rho_array[:, None]**2)
    r_array[r_array == 0] = 1e-20
    ct = dz_array[None, :] / r_array
    st = rho_array[:, None] / r_array
    legendre, _, _ = sf.legendre_normalized(ct, st, 2 * l_max)

    bessel_h = []
    for dm in tqdm(
            range(2 * l_max + 1),
            desc='Spherical Hankel lookup   ',
            file=sys.stdout,
            bar_format='{l_bar}{bar}| elapsed: {elapsed} remaining: {remaining}'
    ):
        bessel_h.append(sf.spherical_hankel(dm, k_is * r_array))

    pbar = tqdm(
        total=blocksize**2,
        desc='Direct coupling           ',
        file=sys.stdout,
        bar_format='{l_bar}{bar}| elapsed: {elapsed} remaining: {remaining}')

    for m1 in range(-m_max, m_max + 1):
        for m2 in range(-m_max, m_max + 1):
            for l1 in range(max(1, abs(m1)), l_max + 1):
                for l2 in range(max(1, abs(m2)), l_max + 1):
                    A = np.zeros((len_rho, len_dz), dtype=complex)
                    B = np.zeros((len_rho, len_dz), dtype=complex)
                    for ld in range(max(abs(l1 - l2), abs(m1 - m2)),
                                    l1 + l2 + 1):  # if ld<abs(m1-m2) then P=0
                        a5, b5 = trf.ab5_coefficients(
                            l2, m2, l1, m1, ld)  # remember that w = A.T
                        A += a5 * bessel_h[ld] * legendre[ld][abs(
                            m1 - m2)]  # remember that w = A.T
                        B += b5 * bessel_h[ld] * legendre[ld][abs(
                            m1 - m2)]  # remember that w = A.T
                    for tau1 in range(2):
                        n1 = smuthi.fields.multi_to_single_index(
                            tau1, l1, m1, l_max, m_max)
                        for tau2 in range(2):
                            n2 = smuthi.fields.multi_to_single_index(
                                tau2, l2, m2, l_max, m_max)
                            if tau1 == tau2:
                                w[:, :, n1, n2] = A
                            else:
                                w[:, :, n1, n2] = B
                            pbar.update()
    pbar.close()

    # switch off direct coupling contribution near rho=0:
    w[rho_array <
      particle_rho_array[~np.eye(particle_rho_array.shape[0], dtype=bool)].min(
      ) / 2, :, :, :] = 0

    # layer mediated ---------------------------------------------------------------------------------------------------
    sys.stdout.write('Layer mediated coupling   : ...')
    sys.stdout.flush()
    if type(k_parallel) == str and k_parallel == 'default':
        k_parallel = smuthi.fields.default_Sommerfeld_k_parallel_array
    kz_is = smuthi.fields.k_z(k_parallel=k_parallel, k=k_is)
    len_kp = len(k_parallel)

    # phase factors
    epljksz = np.exp(1j * kz_is[None, :] *
                     (sz_array[:, None] - 2 * z_is))  # z, k
    emnjksz = np.exp(-1j * kz_is[None, :] * (sz_array[:, None] - 2 * z_is))
    epljkdz = np.exp(1j * kz_is[None, :] * dz_array[:, None])
    emnjkdz = np.exp(-1j * kz_is[None, :] * dz_array[:, None])

    # layer response
    L = np.zeros((2, 2, 2, len_kp), dtype=complex)  # pol, pl/mn1, pl/mn2, kp
    for pol in range(2):
        L[pol, :, :, :] = lay.layersystem_response_matrix(
            pol, layer_system.thicknesses,
            layer_system.refractive_indices, k_parallel,
            smuthi.fields.angular_frequency(vacuum_wavelength), i_s, i_s)

    # transformation coefficients
    B_dag = np.zeros((2, 2, blocksize, len_kp),
                     dtype=complex)  # pol, pl/mn, n, kp
    B = np.zeros((2, 2, blocksize, len_kp), dtype=complex)  # pol, pl/mn, n, kp
    ct_k = kz_is / k_is
    st_k = k_parallel / k_is
    _, pilm_pl, taulm_pl = sf.legendre_normalized(ct_k, st_k, l_max)
    _, pilm_mn, taulm_mn = sf.legendre_normalized(-ct_k, st_k, l_max)

    m_list = [None for i in range(blocksize)]
    for tau in range(2):
        for m in range(-m_max, m_max + 1):
            for l in range(max(1, abs(m)), l_max + 1):
                n = smuthi.fields.multi_to_single_index(
                    tau, l, m, l_max, m_max)
                m_list[n] = m
                for pol in range(2):
                    B_dag[pol, 0, n, :] = trf.transformation_coefficients_vwf(
                        tau,
                        l,
                        m,
                        pol,
                        pilm_list=pilm_pl,
                        taulm_list=taulm_pl,
                        dagger=True)
                    B_dag[pol, 1, n, :] = trf.transformation_coefficients_vwf(
                        tau,
                        l,
                        m,
                        pol,
                        pilm_list=pilm_mn,
                        taulm_list=taulm_mn,
                        dagger=True)
                    B[pol, 0, n, :] = trf.transformation_coefficients_vwf(
                        tau,
                        l,
                        m,
                        pol,
                        pilm_list=pilm_pl,
                        taulm_list=taulm_pl,
                        dagger=False)
                    B[pol, 1, n, :] = trf.transformation_coefficients_vwf(
                        tau,
                        l,
                        m,
                        pol,
                        pilm_list=pilm_mn,
                        taulm_list=taulm_mn,
                        dagger=False)

    # pairs of (n1, n2), listed by abs(m1-m2)
    n1n2_combinations = [[] for dm in range(2 * m_max + 1)]
    for n1 in range(blocksize):
        m1 = m_list[n1]
        for n2 in range(blocksize):
            m2 = m_list[n2]
            n1n2_combinations[abs(m1 - m2)].append((n1, n2))

    wr_pl = np.zeros((len_rho, len_dz, blocksize, blocksize),
                     dtype=np.complex64)
    wr_mn = np.zeros((len_rho, len_dz, blocksize, blocksize),
                     dtype=np.complex64)

    dkp = np.diff(k_parallel)
    if cu.use_gpu:
        re_dkp_d = cu.gpuarray.to_gpu(np.float32(dkp.real))
        im_dkp_d = cu.gpuarray.to_gpu(np.float32(dkp.imag))
        kernel_source_code = cusrc.volume_lookup_assembly_code % (
            blocksize, len_rho, len_sz, len_kp)
        helper_function = cu.SourceModule(kernel_source_code).get_function(
            "helper")
        cuda_blocksize = 128
        cuda_gridsize = (len_rho * len_sz + cuda_blocksize -
                         1) // cuda_blocksize

        re_dwr_d = cu.gpuarray.to_gpu(
            np.zeros((len_rho, len_sz), dtype=np.float32))
        im_dwr_d = cu.gpuarray.to_gpu(
            np.zeros((len_rho, len_sz), dtype=np.float32))

    pbar = tqdm(
        total=blocksize**2,
        desc='Layer mediated coupling   ',
        file=sys.stdout,
        bar_format='{l_bar}{bar}| elapsed: {elapsed} remaining: {remaining}')

    for dm in range(2 * m_max + 1):
        bessel = scipy.special.jv(dm,
                                  (k_parallel[None, :] * rho_array[:, None]))
        besjac = bessel * (k_parallel / (kz_is * k_is))[None, :]

        for n1n2 in n1n2_combinations[dm]:
            n1 = n1n2[0]
            m1 = m_list[n1]
            n2 = n1n2[1]
            m2 = m_list[n2]

            belbee_pl = np.zeros((len_dz, len_kp), dtype=complex)
            belbee_mn = np.zeros((len_dz, len_kp), dtype=complex)
            for pol in range(2):
                belbee_pl += ((L[pol, 0, 1, :] * B_dag[pol, 0, n1, :] *
                               B[pol, 1, n2, :])[None, :] * epljksz +
                              (L[pol, 1, 0, :] * B_dag[pol, 1, n1, :] *
                               B[pol, 0, n2, :])[None, :] * emnjksz)
                belbee_mn += ((L[pol, 0, 0, :] * B_dag[pol, 0, n1, :] *
                               B[pol, 0, n2, :])[None, :] * epljkdz +
                              (L[pol, 1, 1, :] * B_dag[pol, 1, n1, :] *
                               B[pol, 1, n2, :])[None, :] * emnjkdz)

            if cu.use_gpu:
                re_belbee_pl_d = cu.gpuarray.to_gpu(
                    np.float32(belbee_pl[None, :, :].real))
                im_belbee_pl_d = cu.gpuarray.to_gpu(
                    np.float32(belbee_pl[None, :, :].imag))
                re_belbee_mn_d = cu.gpuarray.to_gpu(
                    np.float32(belbee_mn[None, :, :].real))
                im_belbee_mn_d = cu.gpuarray.to_gpu(
                    np.float32(belbee_mn[None, :, :].imag))

                re_besjac_d = cu.gpuarray.to_gpu(
                    np.float32(besjac[:, None, :].real))
                im_besjac_d = cu.gpuarray.to_gpu(
                    np.float32(besjac[:, None, :].imag))
                helper_function(re_besjac_d.gpudata,
                                im_besjac_d.gpudata,
                                re_belbee_pl_d.gpudata,
                                im_belbee_pl_d.gpudata,
                                re_dkp_d.gpudata,
                                im_dkp_d.gpudata,
                                re_dwr_d.gpudata,
                                im_dwr_d.gpudata,
                                block=(cuda_blocksize, 1, 1),
                                grid=(cuda_gridsize, 1))
                wr_pl[:, :, n1,
                      n2] = 4 * (1j)**abs(m2 - m1) * (re_dwr_d.get() +
                                                      1j * im_dwr_d.get())

                helper_function(re_besjac_d.gpudata,
                                im_besjac_d.gpudata,
                                re_belbee_mn_d.gpudata,
                                im_belbee_mn_d.gpudata,
                                re_dkp_d.gpudata,
                                im_dkp_d.gpudata,
                                re_dwr_d.gpudata,
                                im_dwr_d.gpudata,
                                block=(cuda_blocksize, 1, 1),
                                grid=(cuda_gridsize, 1))
                wr_mn[:, :, n1,
                      n2] = 4 * (1j)**abs(m2 - m1) * (re_dwr_d.get() +
                                                      1j * im_dwr_d.get())
            else:
                integrand = besjac[:, None, :] * belbee_pl[None, :, :]
                wr_pl[:, :, n1, n2] = 2 * (1j)**abs(m2 - m1) * (
                    (integrand[:, :, :-1] + integrand[:, :, 1:]) *
                    dkp[None, None, :]).sum(axis=-1)  # trapezoidal rule

                integrand = besjac[:, None, :] * belbee_mn[None, :, :]
                wr_mn[:, :, n1, n2] = 2 * (1j)**abs(m2 - m1) * (
                    (integrand[:, :, :-1] + integrand[:, :, 1:]) *
                    dkp[None, None, :]).sum(axis=-1)
            pbar.update()
    pbar.close()

    return wr_pl, w + wr_mn, rho_array, sz_array, dz_array
Exemple #8
0
def swe_to_pwe_conversion(swe, k_parallel, azimuthal_angles, layer_system=None, layer_number=None,
                          layer_system_mediated=False):
    """Convert SphericalWaveExpansion object to a PlaneWaveExpansion object.

    Args:
        swe (SphericalWaveExpansion):             Spherical wave expansion to be converted
        k_parallel (numpy array or str):          In-plane wavenumbers for the pwe object.
        azimuthal_angles (numpy array or str):    Azimuthal angles for the pwe object
        layer_system (smuthi.layers.LayerSystem): Stratified medium in which the origin of the SWE is located
        layer_number (int):                       Layer number in which the PWE should be valid.
        layer_system_mediated (bool):             If True, the PWE refers to the layer system response of the SWE, 
                                                  otherwise it is the direct transform.

    Returns:
        Tuple of two PlaneWaveExpansion objects, first upgoing, second downgoing.
    """
    # todo: manage diverging swe
    k_parallel = np.array(k_parallel, ndmin=1)
    
    i_swe = layer_system.layer_number(swe.reference_point[2])
    if layer_number is None and not layer_system_mediated:
        layer_number = i_swe
    reference_point = [0, 0, layer_system.reference_z(i_swe)]
    lower_z_up = swe.reference_point[2]
    upper_z_up = layer_system.upper_zlimit(layer_number)
    pwe_up = fex.PlaneWaveExpansion(k=swe.k, k_parallel=k_parallel, azimuthal_angles=azimuthal_angles, kind='upgoing',
                                    reference_point=reference_point, lower_z=lower_z_up, upper_z=upper_z_up)
    lower_z_down = layer_system.lower_zlimit(layer_number)
    upper_z_down = swe.reference_point[2]
    pwe_down = fex.PlaneWaveExpansion(k=swe.k, k_parallel=k_parallel, azimuthal_angles=azimuthal_angles,
                                      kind='downgoing', reference_point=reference_point, lower_z=lower_z_down,
                                      upper_z=upper_z_down)

    agrid = pwe_up.azimuthal_angle_grid()
    kpgrid = pwe_up.k_parallel_grid()
    kx = kpgrid * np.cos(agrid)
    ky = kpgrid * np.sin(agrid)
    kz_up = pwe_up.k_z_grid()
    kz_down = pwe_down.k_z_grid()

    kzvec = pwe_up.k_z()

    kvec_up = np.array([kx, ky, kz_up])
    kvec_down = np.array([kx, ky, kz_down])
    rpwe_mn_rswe = np.array(reference_point) - np.array(swe.reference_point)

    # phase factor for the translation of the reference point from rvec_S to rvec_iS
    ejkrSiS_up = np.exp(1j * np.tensordot(kvec_up, rpwe_mn_rswe, axes=([0], [0])))
    ejkrSiS_down = np.exp(1j * np.tensordot(kvec_down, rpwe_mn_rswe, axes=([0], [0])))
    
    ct_up = pwe_up.k_z() / swe.k
    st_up = pwe_up.k_parallel / swe.k
    plm_list_up, pilm_list_up, taulm_list_up = mathfunc.legendre_normalized(ct_up, st_up, swe.l_max)

    ct_down = pwe_down.k_z() / swe.k
    st_down = pwe_down.k_parallel / swe.k
    plm_list_down, pilm_list_down, taulm_list_down = mathfunc.legendre_normalized(ct_down, st_down, swe.l_max)
    
    for m in range(-swe.m_max, swe.m_max + 1):
        eima = np.exp(1j * m * pwe_up.azimuthal_angles)  # indices: alpha_idx
        for pol in range(2):
            dbB_up = np.zeros(len(k_parallel), dtype=complex)
            dbB_down = np.zeros(len(k_parallel), dtype=complex)
            for l in range(max(1, abs(m)), swe.l_max + 1):
                for tau in range(2):
                    dbB_up += swe.coefficients_tlm(tau, l, m) * transformation_coefficients_vwf(
                        tau, l, m, pol, pilm_list=pilm_list_up, taulm_list=taulm_list_up)
                    dbB_down += swe.coefficients_tlm(tau, l, m) * transformation_coefficients_vwf(
                        tau, l, m, pol, pilm_list=pilm_list_down, taulm_list=taulm_list_down)
            pwe_up.coefficients[pol, :, :] += dbB_up[:, None] * eima[None, :]
            pwe_down.coefficients[pol, :, :] += dbB_down[:, None] * eima[None, :]

    pwe_up.coefficients = pwe_up.coefficients / (2 * np.pi * kzvec[None, :, None] * swe.k) * ejkrSiS_up[None, :, :]
    pwe_down.coefficients = (pwe_down.coefficients / (2 * np.pi * kzvec[None, :, None] * swe.k)
                             * ejkrSiS_down[None, :, :])

    if layer_system_mediated:
        pwe_up, pwe_down = layer_system.response((pwe_up, pwe_down), i_swe, layer_number)

    return pwe_up, pwe_down
Exemple #9
0
def direct_coupling_block_pvwf_mediated(vacuum_wavelength, receiving_particle,
                                        emitting_particle, layer_system,
                                        k_parallel):
    """Direct particle coupling matrix :math:`W` for two particles (via plane vector wave functions).
    For details, see: 
    Dominik Theobald et al., Phys. Rev. A 96, 033822, DOI: 10.1103/PhysRevA.96.033822 or arXiv:1708.04808 


    Args:
        vacuum_wavelength (float):                          Vacuum wavelength :math:`\lambda` (length unit)
        receiving_particle (smuthi.particles.Particle):     Particle that receives the scattered field
        emitting_particle (smuthi.particles.Particle):      Particle that emits the scattered field
        layer_system (smuthi.layers.LayerSystem):           Stratified medium in which the coupling takes place
        k_parallel (numpy.array):                           In-plane wavenumber for plane wave expansion

    Returns:
        Direct coupling matrix block (numpy array).
    """
    if type(receiving_particle).__name__ != 'Spheroid' or type(
            emitting_particle).__name__ != 'Spheroid':
        raise NotImplementedError(
            'plane wave coupling currently implemented only for spheroids')

    lmax1 = receiving_particle.l_max
    mmax1 = receiving_particle.m_max
    assert lmax1 == mmax1, 'PVWF coupling requires lmax == mmax for each particle.'
    lmax2 = emitting_particle.l_max
    mmax2 = emitting_particle.m_max
    assert lmax2 == mmax2, 'PVWF coupling requires lmax == mmax for each particle.'
    lmax = max([lmax1, lmax2])
    m_max = max([mmax1, mmax2])
    blocksize1 = flds.blocksize(lmax1, mmax1)
    blocksize2 = flds.blocksize(lmax2, mmax2)

    n_medium = layer_system.refractive_indices[layer_system.layer_number(
        receiving_particle.position[2])]

    # finding the orientation of a plane separating the spheroids
    _, _, alpha, beta = spheroids_closest_points(
        emitting_particle.semi_axis_a, emitting_particle.semi_axis_c,
        emitting_particle.position, emitting_particle.euler_angles,
        receiving_particle.semi_axis_a, receiving_particle.semi_axis_c,
        receiving_particle.position, receiving_particle.euler_angles)

    # positions
    r1 = np.array(receiving_particle.position)
    r2 = np.array(emitting_particle.position)
    r21_lab = r1 - r2  # laboratory coordinate system

    # distance vector in rotated coordinate system
    r21_rot = np.dot(
        np.dot([[np.cos(beta), 0, np.sin(beta)], [0, 1, 0],
                [-np.sin(beta), 0, np.cos(beta)]],
               [[np.cos(alpha), -np.sin(alpha), 0],
                [np.sin(alpha), np.cos(alpha), 0], [0, 0, 1]]), r21_lab)
    rho21 = (r21_rot[0]**2 + r21_rot[1]**2)**0.5
    phi21 = np.arctan2(r21_rot[1], r21_rot[0])
    z21 = r21_rot[2]

    # wavenumbers
    omega = flds.angular_frequency(vacuum_wavelength)
    k = omega * n_medium
    kz = flds.k_z(k_parallel=k_parallel,
                  vacuum_wavelength=vacuum_wavelength,
                  refractive_index=n_medium)
    if z21 < 0:
        kz_var = -kz
    else:
        kz_var = kz

    # Bessel lookup
    bessel_list = []
    for dm in range(mmax1 + mmax2 + 1):
        bessel_list.append(scipy.special.jn(dm, k_parallel * rho21))

    # legendre function lookups
    ct = kz_var / k
    st = k_parallel / k
    _, pilm_list, taulm_list = sf.legendre_normalized(ct, st, lmax)

    # initialize result
    w = np.zeros((blocksize1, blocksize2), dtype=complex)

    # prefactor
    const_arr = k_parallel / (kz * k) * np.exp(1j * (kz_var * z21))

    for m1 in range(-mmax1, mmax1 + 1):
        for m2 in range(-mmax2, mmax2 + 1):
            jmm_eimphi_bessel = 4 * 1j**abs(m2 - m1) * np.exp(
                1j * phi21 * (m2 - m1)) * bessel_list[abs(m2 - m1)]
            prefactor = const_arr * jmm_eimphi_bessel
            for l1 in range(max(1, abs(m1)), lmax1 + 1):
                for l2 in range(max(1, abs(m2)), lmax2 + 1):
                    for tau1 in range(2):
                        n1 = flds.multi_to_single_index(
                            tau1, l1, m1, lmax1, mmax1)
                        for tau2 in range(2):
                            n2 = flds.multi_to_single_index(
                                tau2, l2, m2, lmax2, mmax2)
                            for pol in range(2):
                                B_dag = trf.transformation_coefficients_vwf(
                                    tau1,
                                    l1,
                                    m1,
                                    pol,
                                    pilm_list=pilm_list,
                                    taulm_list=taulm_list,
                                    dagger=True)
                                B = trf.transformation_coefficients_vwf(
                                    tau2,
                                    l2,
                                    m2,
                                    pol,
                                    pilm_list=pilm_list,
                                    taulm_list=taulm_list,
                                    dagger=False)
                                integrand = prefactor * B * B_dag
                                w[n1, n2] += np.trapz(integrand, k_parallel)

    rot_mat_1 = trf.block_rotation_matrix_D_svwf(lmax1, mmax1, 0, beta, alpha)
    rot_mat_2 = trf.block_rotation_matrix_D_svwf(lmax2, mmax2, -alpha, -beta,
                                                 0)

    return np.dot(np.dot(np.transpose(rot_mat_1), w), np.transpose(rot_mat_2))
Exemple #10
0
def direct_coupling_block(vacuum_wavelength, receiving_particle,
                          emitting_particle, layer_system):
    """Direct particle coupling matrix :math:`W` for two particles. This routine is explicit, but slow.

    Args:
        vacuum_wavelength (float):                          Vacuum wavelength :math:`\lambda` (length unit)
        receiving_particle (smuthi.particles.Particle):     Particle that receives the scattered field
        emitting_particle (smuthi.particles.Particle):      Particle that emits the scattered field
        layer_system (smuthi.layers.LayerSystem):           Stratified medium in which the coupling takes place

    Returns:
        Direct coupling matrix block as numpy array.
    """
    omega = flds.angular_frequency(vacuum_wavelength)

    # index specs
    lmax1 = receiving_particle.l_max
    mmax1 = receiving_particle.m_max
    lmax2 = emitting_particle.l_max
    mmax2 = emitting_particle.m_max
    blocksize1 = flds.blocksize(lmax1, mmax1)
    blocksize2 = flds.blocksize(lmax2, mmax2)

    # initialize result
    w = np.zeros((blocksize1, blocksize2), dtype=complex)

    # check if particles are in same layer
    rS1 = receiving_particle.position
    rS2 = emitting_particle.position
    iS1 = layer_system.layer_number(rS1[2])
    iS2 = layer_system.layer_number(rS2[2])
    if iS1 == iS2 and not emitting_particle == receiving_particle:
        k = omega * layer_system.refractive_indices[iS1]
        dx = rS1[0] - rS2[0]
        dy = rS1[1] - rS2[1]
        dz = rS1[2] - rS2[2]
        d = np.sqrt(dx**2 + dy**2 + dz**2)
        cos_theta = dz / d
        sin_theta = np.sqrt(dx**2 + dy**2) / d
        phi = np.arctan2(dy, dx)

        # spherical functions
        bessel_h = [
            sf.spherical_hankel(n, k * d) for n in range(lmax1 + lmax2 + 1)
        ]
        legendre, _, _ = sf.legendre_normalized(cos_theta, sin_theta,
                                                lmax1 + lmax2)

        # the particle coupling operator is the transpose of the SVWF translation operator
        # therefore, (l1,m1) and (l2,m2) are interchanged:
        for m1 in range(-mmax1, mmax1 + 1):
            for m2 in range(-mmax2, mmax2 + 1):
                eimph = np.exp(1j * (m2 - m1) * phi)
                for l1 in range(max(1, abs(m1)), lmax1 + 1):
                    for l2 in range(max(1, abs(m2)), lmax2 + 1):
                        A, B = complex(0), complex(0)
                        for ld in range(max(abs(l1 - l2), abs(m1 - m2)), l1 +
                                        l2 + 1):  # if ld<abs(m1-m2) then P=0
                            a5, b5 = trf.ab5_coefficients(l2, m2, l1, m1, ld)
                            A += a5 * bessel_h[ld] * legendre[ld][abs(m1 - m2)]
                            B += b5 * bessel_h[ld] * legendre[ld][abs(m1 - m2)]
                        A, B = eimph * A, eimph * B
                        for tau1 in range(2):
                            n1 = flds.multi_to_single_index(
                                tau1, l1, m1, lmax1, mmax1)
                            for tau2 in range(2):
                                n2 = flds.multi_to_single_index(
                                    tau2, l2, m2, lmax2, mmax2)
                                if tau1 == tau2:
                                    w[n1, n2] = A
                                else:
                                    w[n1, n2] = B

    return w
def layer_mediated_coupling_block(vacuum_wavelength,
                                  receiving_particle,
                                  emitting_particle,
                                  layer_system,
                                  k_parallel='default',
                                  show_integrand=False):
    """Layer-system mediated particle coupling matrix :math:`W^R` for two particles. This routine is explicit, but slow.

    Args:
        vacuum_wavelength (float):                          Vacuum wavelength :math:`\lambda` (length unit)
        receiving_particle (smuthi.particles.Particle):     Particle that receives the scattered field
        emitting_particle (smuthi.particles.Particle):      Particle that emits the scattered field
        layer_system (smuthi.layers.LayerSystem):           Stratified medium in which the coupling takes place
        k_parallel (numpy ndarray):                         In-plane wavenumbers for Sommerfeld integral
                                                            If 'default', use smuthi.fields.default_Sommerfeld_k_parallel_array
        show_integrand (bool):                              If True, the norm of the integrand is plotted.

    Returns:
        Layer mediated coupling matrix block as numpy array.
    """
    if type(k_parallel) == str and k_parallel == 'default':
        k_parallel = smuthi.fields.default_Sommerfeld_k_parallel_array

    omega = smuthi.fields.angular_frequency(vacuum_wavelength)

    # index specs
    lmax1 = receiving_particle.l_max
    mmax1 = receiving_particle.m_max
    lmax2 = emitting_particle.l_max
    mmax2 = emitting_particle.m_max
    blocksize1 = smuthi.fields.blocksize(lmax1, mmax1)
    blocksize2 = smuthi.fields.blocksize(lmax2, mmax2)

    # cylindrical coordinates of relative position vectors
    rs1 = np.array(receiving_particle.position)
    rs2 = np.array(emitting_particle.position)
    rs2s1 = rs1 - rs2
    rhos2s1 = np.linalg.norm(rs2s1[0:2])
    phis2s1 = np.arctan2(rs2s1[1], rs2s1[0])
    is1 = layer_system.layer_number(rs1[2])
    ziss1 = rs1[2] - layer_system.reference_z(is1)
    is2 = layer_system.layer_number(rs2[2])
    ziss2 = rs2[2] - layer_system.reference_z(is2)

    # wave numbers
    kis1 = omega * layer_system.refractive_indices[is1]
    kis2 = omega * layer_system.refractive_indices[is2]
    kzis1 = smuthi.fields.k_z(k_parallel=k_parallel, k=kis1)
    kzis2 = smuthi.fields.k_z(k_parallel=k_parallel, k=kis2)

    # phase factors
    ejkz = np.zeros(
        (2, 2, len(k_parallel)),
        dtype=complex)  # indices are: particle, plus/minus, kpar_idx
    ejkz[0, 0, :] = np.exp(1j * kzis1 * ziss1)
    ejkz[0, 1, :] = np.exp(-1j * kzis1 * ziss1)
    ejkz[1, 0, :] = np.exp(1j * kzis2 * ziss2)
    ejkz[1, 1, :] = np.exp(-1j * kzis2 * ziss2)

    # layer response
    L = np.zeros((2, 2, 2, len(k_parallel)),
                 dtype=complex)  # polarization, pl/mn1, pl/mn2, kpar_idx
    for pol in range(2):
        L[pol, :, :, :] = lay.layersystem_response_matrix(
            pol, layer_system.thicknesses, layer_system.refractive_indices,
            k_parallel, omega, is2, is1)

    # transformation coefficients
    B = [
        np.zeros((2, 2, blocksize1, len(k_parallel)), dtype=complex),
        np.zeros((2, 2, blocksize2, len(k_parallel)), dtype=complex)
    ]
    # list index: particle, np indices: pol, plus/minus, n, kpar_idx

    m_vec = [np.zeros(blocksize1, dtype=int), np.zeros(blocksize2, dtype=int)]

    # precompute spherical functions
    ct = kzis1 / kis1
    st = k_parallel / kis1
    _, pilm_list_pl, taulm_list_pl = sf.legendre_normalized(ct, st, lmax1)
    _, pilm_list_mn, taulm_list_mn = sf.legendre_normalized(-ct, st, lmax1)
    pilm = (pilm_list_pl, pilm_list_mn)
    taulm = (taulm_list_pl, taulm_list_mn)

    for tau in range(2):
        for m in range(-mmax1, mmax1 + 1):
            for l in range(max(1, abs(m)), lmax1 + 1):
                n = smuthi.fields.multi_to_single_index(
                    tau, l, m, lmax1, mmax1)
                m_vec[0][n] = m
                for iplmn in range(2):
                    for pol in range(2):
                        B[0][pol, iplmn,
                             n, :] = trf.transformation_coefficients_vwf(
                                 tau,
                                 l,
                                 m,
                                 pol,
                                 pilm_list=pilm[iplmn],
                                 taulm_list=taulm[iplmn],
                                 dagger=True)

    ct = kzis2 / kis2
    st = k_parallel / kis2
    _, pilm_list_pl, taulm_list_pl = sf.legendre_normalized(ct, st, lmax2)
    _, pilm_list_mn, taulm_list_mn = sf.legendre_normalized(-ct, st, lmax2)
    pilm = (pilm_list_pl, pilm_list_mn)
    taulm = (taulm_list_pl, taulm_list_mn)

    for tau in range(2):
        for m in range(-mmax2, mmax2 + 1):
            for l in range(max(1, abs(m)), lmax2 + 1):
                n = smuthi.fields.multi_to_single_index(
                    tau, l, m, lmax2, mmax2)
                m_vec[1][n] = m
                for iplmn in range(2):
                    for pol in range(2):
                        B[1][pol, iplmn,
                             n, :] = trf.transformation_coefficients_vwf(
                                 tau,
                                 l,
                                 m,
                                 pol,
                                 pilm_list=pilm[iplmn],
                                 taulm_list=taulm[iplmn],
                                 dagger=False)

    # bessel function and jacobi factor
    bessel_list = []
    for dm in range(lmax1 + lmax2 + 1):
        bessel_list.append(scipy.special.jv(dm, k_parallel * rhos2s1))
    jacobi_vector = k_parallel / (kzis2 * kis2)
    m2_minus_m1 = m_vec[1] - m_vec[0][np.newaxis].T
    wr_const = 4 * (1j)**abs(m2_minus_m1) * np.exp(1j * m2_minus_m1 * phis2s1)

    integral = np.zeros((blocksize1, blocksize2), dtype=complex)
    for n1 in range(blocksize1):
        BeL = np.zeros((2, 2, len(k_parallel)),
                       dtype=complex)  # indices are: pol, plmn2, n1, kpar_idx
        for iplmn1 in range(2):
            for pol in range(2):
                BeL[pol, :, :] += (L[pol, iplmn1, :, :] *
                                   B[0][pol, iplmn1, n1, :] *
                                   ejkz[0, iplmn1, :])
        for n2 in range(blocksize2):
            bessel_full = bessel_list[abs(m_vec[0][n1] - m_vec[1][n2])]
            BeLBe = np.zeros((len(k_parallel)), dtype=complex)
            eval_BeLBe(BeLBe, BeL, B[1], ejkz, n2)
            integrand = bessel_full * jacobi_vector * BeLBe
            integral[n1, n2] = numba_trapz(integrand, k_parallel)
    wr = wr_const * integral

    return wr