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
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
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
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
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
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
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))
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