""" Functional versions of many FDFD operators. These can be useful for performing FDFD calculations without needing to construct large matrices in memory. The functions generated here expect `cfdfield_t` inputs with shape (3, X, Y, Z), e.g. E = [E_x, E_y, E_z] where each (complex) component has shape (X, Y, Z) """ from typing import Callable import numpy from ..fdmath import dx_lists_t, fdfield_t, cfdfield_t, cfdfield_updater_t from ..fdmath.functional import curl_forward, curl_back __author__ = 'Jan Petykiewicz' def e_full( omega: complex, dxes: dx_lists_t, epsilon: fdfield_t, mu: fdfield_t | None = None, ) -> cfdfield_updater_t: """ Wave operator for use with E-field. See `operators.e_full` for details. Args: omega: Angular frequency of the simulation dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` epsilon: Dielectric constant mu: Magnetic permeability (default 1 everywhere) Returns: Function `f` implementing the wave operator `f(E)` -> `-i * omega * J` """ ch = curl_back(dxes[1]) ce = curl_forward(dxes[0]) def op_1(e: cfdfield_t) -> cfdfield_t: curls = ch(ce(e)) return curls - omega ** 2 * epsilon * e def op_mu(e: cfdfield_t) -> cfdfield_t: curls = ch(mu * ce(e)) # type: ignore # mu = None ok because we don't return the function return curls - omega ** 2 * epsilon * e if mu is None: return op_1 else: return op_mu def eh_full( omega: complex, dxes: dx_lists_t, epsilon: fdfield_t, mu: fdfield_t | None = None, ) -> Callable[[cfdfield_t, cfdfield_t], tuple[cfdfield_t, cfdfield_t]]: """ Wave operator for full (both E and H) field representation. See `operators.eh_full`. Args: omega: Angular frequency of the simulation dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` epsilon: Dielectric constant mu: Magnetic permeability (default 1 everywhere) Returns: Function `f` implementing the wave operator `f(E, H)` -> `(J, -M)` """ ch = curl_back(dxes[1]) ce = curl_forward(dxes[0]) def op_1(e: cfdfield_t, h: cfdfield_t) -> tuple[cfdfield_t, cfdfield_t]: return (ch(h) - 1j * omega * epsilon * e, ce(e) + 1j * omega * h) def op_mu(e: cfdfield_t, h: cfdfield_t) -> tuple[cfdfield_t, cfdfield_t]: return (ch(h) - 1j * omega * epsilon * e, ce(e) + 1j * omega * mu * h) # type: ignore # mu=None ok if mu is None: return op_1 else: return op_mu def e2h( omega: complex, dxes: dx_lists_t, mu: fdfield_t | None = None, ) -> cfdfield_updater_t: """ Utility operator for converting the `E` field into the `H` field. For use with `e_full` -- assumes that there is no magnetic current `M`. Args: omega: Angular frequency of the simulation dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` mu: Magnetic permeability (default 1 everywhere) Returns: Function `f` for converting `E` to `H`, `f(E)` -> `H` """ ce = curl_forward(dxes[0]) def e2h_1_1(e: cfdfield_t) -> cfdfield_t: return ce(e) / (-1j * omega) def e2h_mu(e: cfdfield_t) -> cfdfield_t: return ce(e) / (-1j * omega * mu) # type: ignore # mu=None ok if mu is None: return e2h_1_1 else: return e2h_mu def m2j( omega: complex, dxes: dx_lists_t, mu: fdfield_t | None = None, ) -> cfdfield_updater_t: """ Utility operator for converting magnetic current `M` distribution into equivalent electric current distribution `J`. For use with e.g. `e_full`. Args: omega: Angular frequency of the simulation dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` mu: Magnetic permeability (default 1 everywhere) Returns: Function `f` for converting `M` to `J`, `f(M)` -> `J` """ ch = curl_back(dxes[1]) def m2j_mu(m: cfdfield_t) -> cfdfield_t: J = ch(m / mu) / (-1j * omega) # type: ignore # mu=None ok return J def m2j_1(m: cfdfield_t) -> cfdfield_t: J = ch(m) / (-1j * omega) return J if mu is None: return m2j_1 else: return m2j_mu def e_tfsf_source( TF_region: fdfield_t, omega: complex, dxes: dx_lists_t, epsilon: fdfield_t, mu: fdfield_t | None = None, ) -> cfdfield_updater_t: """ Operator that turns an E-field distribution into a total-field/scattered-field (TFSF) source. Args: TF_region: mask which is set to 1 in the total-field region, and 0 elsewhere (i.e. in the scattered-field region). Should have the same shape as the simulation grid, e.g. `epsilon[0].shape`. omega: Angular frequency of the simulation dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` epsilon: Dielectric constant distribution mu: Magnetic permeability (default 1 everywhere) Returns: Function `f` which takes an E field and returns a current distribution, `f(E)` -> `J` """ # TODO documentation A = e_full(omega, dxes, epsilon, mu) def op(e: cfdfield_t) -> cfdfield_t: neg_iwj = A(TF_region * e) - TF_region * A(e) return neg_iwj / (-1j * omega) return op def poynting_e_cross_h(dxes: dx_lists_t) -> Callable[[cfdfield_t, cfdfield_t], cfdfield_t]: """ Generates a function that takes the single-frequency `E` and `H` fields and calculates the cross product `E` x `H` = $E \\times H$ as required for the Poynting vector, $S = E \\times H$ Note: This function also shifts the input `E` field by one cell as required for computing the Poynting cross product (see `meanas.fdfd` module docs). Note: If `E` and `H` are peak amplitudes as assumed elsewhere in this code, the time-average of the poynting vector is ` = Re(S)/2 = Re(E x H*) / 2`. The factor of `1/2` can be omitted if root-mean-square quantities are used instead. Args: dxes: Grid parameters `[dx_e, dx_h]` as described in `meanas.fdmath.types` Returns: Function `f` that returns E x H as required for the poynting vector. """ def exh(e: cfdfield_t, h: cfdfield_t) -> cfdfield_t: s = numpy.empty_like(e) ex = e[0] * dxes[0][0][:, None, None] ey = e[1] * dxes[0][1][None, :, None] ez = e[2] * dxes[0][2][None, None, :] hx = h[0] * dxes[1][0][:, None, None] hy = h[1] * dxes[1][1][None, :, None] hz = h[2] * dxes[1][2][None, None, :] s[0] = numpy.roll(ey, -1, axis=0) * hz - numpy.roll(ez, -1, axis=0) * hy s[1] = numpy.roll(ez, -1, axis=1) * hx - numpy.roll(ex, -1, axis=1) * hz s[2] = numpy.roll(ex, -1, axis=2) * hy - numpy.roll(ey, -1, axis=2) * hx return s return exh