From 40677664784477735b7dc3852e2db9c89c58b487 Mon Sep 17 00:00:00 2001 From: jan Date: Mon, 8 Jan 2018 16:16:26 -0800 Subject: [PATCH 01/77] use own CG implementation --- examples/bloch.py | 10 +- fdfd_tools/bloch.py | 378 ++++++++++++++++++++++++++++---------------- 2 files changed, 250 insertions(+), 138 deletions(-) diff --git a/examples/bloch.py b/examples/bloch.py index 8bbd30e..793bd89 100644 --- a/examples/bloch.py +++ b/examples/bloch.py @@ -30,11 +30,11 @@ g2.shifts = numpy.zeros((6,3)) g2.grids = [numpy.zeros(g.shape) for _ in range(6)] epsilon = [g.grids[0],] * 3 -reciprocal_lattice = numpy.diag(1e6/numpy.array([x_period, y_period, z_period])) #cols are vectors +reciprocal_lattice = numpy.diag(1000/numpy.array([x_period, y_period, z_period])) #cols are vectors #print('Finding k at 1550nm') -#k, f = bloch.find_k(frequency=1/1550, -# tolerance=(1/1550 - 1/1551), +#k, f = bloch.find_k(frequency=1000/1550, +# tolerance=(1000 * (1/1550 - 1/1551)), # direction=[1, 0, 0], # G_matrix=reciprocal_lattice, # epsilon=epsilon, @@ -47,10 +47,10 @@ for k0x in [.25]: k0 = numpy.array([k0x, 0, 0]) kmag = norm(reciprocal_lattice @ k0) - tolerance = (1e6/1550) * 1e-4/1.5 # df = f * dn_eff / n + tolerance = (1000/1550) * 1e-4/1.5 # df = f * dn_eff / n logger.info('tolerance {}'.format(tolerance)) - n, v = bloch.eigsolve(4, k0, G_matrix=reciprocal_lattice, epsilon=epsilon, tolerance=tolerance) + n, v = bloch.eigsolve(4, k0, G_matrix=reciprocal_lattice, epsilon=epsilon, tolerance=tolerance**2) v2e = bloch.hmn_2_exyz(k0, G_matrix=reciprocal_lattice, epsilon=epsilon) v2h = bloch.hmn_2_hxyz(k0, G_matrix=reciprocal_lattice, epsilon=epsilon) ki = bloch.generate_kmn(k0, reciprocal_lattice, g.shape) diff --git a/fdfd_tools/bloch.py b/fdfd_tools/bloch.py index b172e21..31d364d 100644 --- a/fdfd_tools/bloch.py +++ b/fdfd_tools/bloch.py @@ -76,6 +76,7 @@ This module contains functions for generating and solving the from typing import List, Tuple, Callable, Dict import logging import numpy +from numpy import pi, real, trace from numpy.fft import fftn, ifftn, fftfreq import scipy import scipy.optimize @@ -337,139 +338,6 @@ def inverse_maxwell_operator_approx(k0: numpy.ndarray, return operator -def eigsolve(num_modes: int, - k0: numpy.ndarray, - G_matrix: numpy.ndarray, - epsilon: field_t, - mu: field_t = None, - tolerance = 1e-8, - ) -> Tuple[numpy.ndarray, numpy.ndarray]: - """ - Find the first (lowest-frequency) num_modes eigenmodes with Bloch wavevector - k0 of the specified structure. - - :param k0: Bloch wavevector, [k0x, k0y, k0z]. - :param G_matrix: 3x3 matrix, with reciprocal lattice vectors as columns. - :param epsilon: Dielectric constant distribution for the simulation. - All fields are sampled at cell centers (i.e., NOT Yee-gridded) - :param mu: Magnetic permability distribution for the simulation. - Default None (1 everywhere). - :return: (eigenvalues, eigenvectors) where eigenvalues[i] corresponds to the - vector eigenvectors[i, :] - """ - h_size = 2 * epsilon[0].size - - kmag = norm(G_matrix @ k0) - - ''' - Generate the operators - ''' - mop = maxwell_operator(k0=k0, G_matrix=G_matrix, epsilon=epsilon, mu=mu) - imop = inverse_maxwell_operator_approx(k0=k0, G_matrix=G_matrix, epsilon=epsilon, mu=mu) - - scipy_op = spalg.LinearOperator(dtype=complex, shape=(h_size, h_size), matvec=mop) - scipy_iop = spalg.LinearOperator(dtype=complex, shape=(h_size, h_size), matvec=imop) - - y_shape = (h_size, num_modes) - - def rayleigh_quotient(Z: numpy.ndarray, approx_grad: bool = True): - """ - Absolute value of the block Rayleigh quotient, and the associated gradient. - - See Johnson and Joannopoulos, Opt. Expr. 8, 3 (2001) for details (full - citation in module docstring). - - === - - Notes on my understanding of the procedure: - - Minimize f(Y) = |trace((Y.H @ A @ Y)|, making use of Y = Z @ inv(Z.H @ Z)^(1/2) - (a polar orthogonalization of Y). This gives f(Z) = |trace(Z.H @ A @ Z @ U)|, - where U = inv(Z.H @ Z). We minimize the absolute value to find the eigenvalues - with smallest magnitude. - - The gradient is P @ (A @ Z @ U), where P = (1 - Z @ U @ Z.H) is a projection - onto the space orthonormal to Z. If approx_grad is True, the approximate - inverse of the maxwell operator is used to precondition the gradient. - """ - z = Z.view(dtype=complex).reshape(y_shape) - U = numpy.linalg.inv(z.conj().T @ z) - zU = z @ U - AzU = scipy_op @ zU - zTAzU = z.conj().T @ AzU - f = numpy.real(numpy.trace(zTAzU)) - if approx_grad: - df_dy = scipy_iop @ (AzU - zU @ zTAzU) - else: - df_dy = (AzU - zU @ zTAzU) - - df_dy_flat = df_dy.view(dtype=float).ravel() - return numpy.abs(f), numpy.sign(f) * df_dy_flat - - ''' - Use the conjugate gradient method and the approximate gradient calculation to - quickly find approximate eigenvectors. - ''' - result = scipy.optimize.minimize(rayleigh_quotient, - numpy.random.rand(*y_shape, 2), - jac=True, - method='L-BFGS-B', - tol=1e-20, - options={'maxiter': 2000, 'gtol':0, 'ftol':1e-20 , 'disp':True})#, 'maxls':80, 'm':30}) - - - result = scipy.optimize.minimize(lambda y: rayleigh_quotient(y, True), - result.x, - jac=True, - method='L-BFGS-B', - tol=1e-20, - options={'maxiter': 2000, 'gtol':0, 'disp':True}) - - result = scipy.optimize.minimize(lambda y: rayleigh_quotient(y, False), - result.x, - jac=True, - method='L-BFGS-B', - tol=1e-20, - options={'maxiter': 2000, 'gtol':0, 'disp':True}) - - for i in range(20): - result = scipy.optimize.minimize(lambda y: rayleigh_quotient(y, False), - result.x, - jac=True, - method='L-BFGS-B', - tol=1e-20, - options={'maxiter': 70, 'gtol':0, 'disp':True}) - if result.nit == 0: - # We took 0 steps, so re-running won't help - break - - - z = result.x.view(dtype=complex).reshape(y_shape) - - ''' - Recover eigenvectors from Z - ''' - U = numpy.linalg.inv(z.conj().T @ z) - y = z @ scipy.linalg.sqrtm(U) - w = y.conj().T @ (scipy_op @ y) - - eigvals, w_eigvecs = numpy.linalg.eig(w) - eigvecs = y @ w_eigvecs - - for i in range(len(eigvals)): - v = eigvecs[:, i] - n = eigvals[i] - v /= norm(v) - eigness = norm(scipy_op @ v - (v.conj() @ (scipy_op @ v)) * v ) - f = numpy.sqrt(-numpy.real(n)) - df = numpy.sqrt(-numpy.real(n + eigness)) - neff_err = kmag * (1/df - 1/f) - logger.info('eigness {}: {}\n neff_err: {}'.format(i, eigness, neff_err)) - - order = numpy.argsort(numpy.abs(eigvals)) - return eigvals[order], eigvecs.T[order] - - def find_k(frequency: float, tolerance: float, direction: numpy.ndarray, @@ -511,3 +379,247 @@ def find_k(frequency: float, return res.x * direction, res.fun + frequency + +def eigsolve(num_modes: int, + k0: numpy.ndarray, + G_matrix: numpy.ndarray, + epsilon: field_t, + mu: field_t = None, + tolerance = 1e-20, + ) -> Tuple[numpy.ndarray, numpy.ndarray]: + """ + Find the first (lowest-frequency) num_modes eigenmodes with Bloch wavevector + k0 of the specified structure. + + :param k0: Bloch wavevector, [k0x, k0y, k0z]. + :param G_matrix: 3x3 matrix, with reciprocal lattice vectors as columns. + :param epsilon: Dielectric constant distribution for the simulation. + All fields are sampled at cell centers (i.e., NOT Yee-gridded) + :param mu: Magnetic permability distribution for the simulation. + Default None (1 everywhere). + :param tolerance: Solver stops when fractional change in the objective + trace(Z.H @ A @ Z @ inv(Z Z.H)) is smaller than the tolerance + :return: (eigenvalues, eigenvectors) where eigenvalues[i] corresponds to the + vector eigenvectors[i, :] + """ + h_size = 2 * epsilon[0].size + + kmag = norm(G_matrix @ k0) + + ''' + Generate the operators + ''' + mop = maxwell_operator(k0=k0, G_matrix=G_matrix, epsilon=epsilon, mu=mu) + imop = inverse_maxwell_operator_approx(k0=k0, G_matrix=G_matrix, epsilon=epsilon, mu=mu) + + scipy_op = spalg.LinearOperator(dtype=complex, shape=(h_size, h_size), matvec=mop) + scipy_iop = spalg.LinearOperator(dtype=complex, shape=(h_size, h_size), matvec=imop) + + y_shape = (h_size, num_modes) + + prev_E = 0 + d_scale = 1 + prev_traceGtKG = 0 + prev_theta = 0.5 + D = numpy.zeros(shape=y_shape, dtype=complex) + + y0 = None + if y0 is None: + Z = numpy.random.rand(*y_shape).astype(complex) + else: + Z = y0 + + while True: + Z2 = Z.conj().T @ Z + Z_norm = numpy.sqrt(real(trace(Z2))) / num_modes + Z /= Z_norm + Z2 /= Z_norm * Z_norm + try: + U = numpy.linalg.inv(Z2) + except numpy.linalg.LinAlgError: + Z = numpy.random.rand(*y_shape).astype(complex) + continue + + trace_U = real(trace(U)) + if trace_U > 1e8 * num_modes: + Z = Z @ scipy.linalg.sqrtm(U).conj().T + prev_traceGtKG = 0 + continue + break + + def rtrace_AtB(A, B): + return real(numpy.sum(A.conj() * B)) + + def symmetrize(A): + return (A + A.conj().T) * 0.5 + + max_iters = 10000 + for iter in range(max_iters): + U = numpy.linalg.inv(Z.conj().T @ Z) + AZ = scipy_op @ Z + AZU = AZ @ U + ZtAZU = Z.conj().T @ AZU + E = real(trace(ZtAZU)) + sgn = numpy.sign(E) + E = numpy.abs(E) + G = (AZU - Z @ U @ ZtAZU) * sgn + + if iter > 0 and abs(E - prev_E) < tolerance * 0.5 * (E + prev_E + 1e-7): + logging.info('Optimization succeded: {} - 5e-8 < {} * {} / 2'.format(abs(E - prev_E), tolerance, E + prev_E)) + break + + KG = scipy_iop @ G + traceGtKG = rtrace_AtB(G, KG) + gamma_numerator = traceGtKG + + reset_iters = 100 + if prev_traceGtKG == 0 or iter % reset_iters == 0: + print('RESET!') + gamma = 0 + else: + gamma = gamma_numerator / prev_traceGtKG + + D = gamma * d_scale * D + KG + d_scale = numpy.sqrt(rtrace_AtB(D, D)) / num_modes + D /= d_scale + + AD = scipy_op @ D + DtD = D.conj().T @ D + DtAD = D.conj().T @ AD + + ZtD = Z.conj().T @ D + ZtAD = Z.conj().T @ AD + symZtD = symmetrize(ZtD) + symZtAD = symmetrize(ZtAD) + + U_sZtD = U @ symZtD + + dE = 2.0 * (rtrace_AtB(U, symZtAD) - rtrace_AtB(ZtAZU, U_sZtD)) + + S2 = DtD - 4 * symZtD @ U_sZtD + d2E = 2 * (rtrace_AtB(U, DtAD) - + rtrace_AtB(ZtAZU, U @ S2) - + 4 * rtrace_AtB(U, symZtAD @ U_sZtD)) + + # Newton-Raphson to find a root of the first derivative: + theta = -dE/d2E + + if d2E < 0 or abs(theta) >= pi: + theta = -abs(prev_theta) * numpy.sign(dE) + + # ZtAZU * ZtZ = ZtAZ for use in line search + ZtZ = Z.conj().T @ Z + ZtAZ = ZtAZU @ ZtZ.conj().T + + def Qi_func(theta, memo=[None, None]): + if memo[0] == theta: + return memo[1] + + c = numpy.cos(theta) + s = numpy.sin(theta) + Q = c*c * ZtZ + s*s * DtD + 2*s*c * symZtD + try: + Qi = numpy.linalg.inv(Q) + except numpy.linalg.LinAlgError: + logger.info('taylor Qi') + # if c or s small, taylor expand + if c < 1e-4 * s and c != 0: + Qi = numpy.linalg.inv(DtD) + Qi = Qi / (s*s) - 2*c/(s*s*s) * (Qi @ symZtD.conj().T @ Qi.conj().T) + elif s < 1e-4 * c and s != 0: + Qi = numpy.linalg.inv(ZtZ) + Qi = Qi / (c*c) - 2*s/(c*c*c) * (Qi @ symZtD.conj().T @ Qi.conj().T) + else: + raise Exception('Inexplicable singularity in trace_func') + memo[0] = theta + memo[1] = Qi + return Qi + + def trace_func(theta): + c = numpy.cos(theta) + s = numpy.sin(theta) + Qi = Qi_func(theta) + R = c*c * ZtAZ + s*s * DtAD + 2*s*c * symZtAD + trace = rtrace_AtB(R, Qi) + return numpy.abs(trace) + + #def trace_deriv(theta): + # Qi = Qi_func(theta) + # c2 = numpy.cos(2 * theta) + # s2 = numpy.sin(2 * theta) + # F = -0.5*s2 * (ZtAZ - DtAD) + c2 * symZtAD + # trace_deriv = rtrace_AtB(Qi, F) + + # G = Qi @ F.conj().T @ Qi.conj().T + # H = -0.5*s2 * (ZtZ - DtD) + c2 * symZtD + # trace_deriv -= rtrace_AtB(G, H) + + # trace_deriv *= 2 + # return trace_deriv * sgn + + ''' + theta, new_E, new_dE = linmin(theta, E, dE, 0.1, min(tolerance, 1e-6), 1e-14, 0, -numpy.sign(dE) * K_PI, trace_func) + ''' + #theta, n, _, new_E, _, _new_dE = scipy.optimize.line_search(trace_func, trace_deriv, xk=theta, pk=numpy.ones((1,1)), gfk=dE, old_fval=E, c1=min(tolerance, 1e-6), c2=0.1, amax=pi) + result = scipy.optimize.minimize_scalar(trace_func, bounds=(0, pi), tol=tolerance) + new_E = result.fun + theta = result.x + + improvement = numpy.abs(E - new_E) * 2 / numpy.abs(E + new_E) + logger.info('linmin improvement {}'.format(improvement)) + Z *= numpy.cos(theta) + Z += D * numpy.sin(theta) + + prev_traceGtKG = traceGtKG + prev_theta = theta + prev_E = E + + ''' + Recover eigenvectors from Z + ''' + U = numpy.linalg.inv(Z.conj().T @ Z) + Y = Z @ scipy.linalg.sqrtm(U) + W = Y.conj().T @ (scipy_op @ Y) + + eigvals, W_eigvecs = numpy.linalg.eig(W) + eigvecs = Y @ W_eigvecs + + for i in range(len(eigvals)): + v = eigvecs[:, i] + n = eigvals[i] + v /= norm(v) + eigness = norm(scipy_op @ v - (v.conj() @ (scipy_op @ v)) * v ) + f = numpy.sqrt(-numpy.real(n)) + df = numpy.sqrt(-numpy.real(n + eigness)) + neff_err = kmag * (1/df - 1/f) + logger.info('eigness {}: {}\n neff_err: {}'.format(i, eigness, neff_err)) + + order = numpy.argsort(numpy.abs(eigvals)) + return eigvals[order], eigvecs.T[order] + + #def linmin(x_guess, f0, df0, x_max, f_tol=0.1, df_tol=min(tolerance, 1e-6), x_tol=1e-14, x_min=0, linmin_func): + # if df0 > 0: + # x0, f0, df0 = linmin(-x_guess, f0, -df0, -x_max, f_tol, df_tol, x_tol, -x_min, lambda q, dq: -linmin_func(q, dq)) + # return -x0, f0, -df0 + # elif df0 == 0: + # return 0, f0, df0 + # else: + # x = x_guess + # fx = f0 + # dfx = df0 + + # isave = numpy.zeros((2,), numpy.intc) + # dsave = numpy.zeros((13,), float) + + # x, fx, dfx, task = minpack2.dsrch(x, fx, dfx, f_tol, df_tol, x_tol, task, + # x_min, x_max, isave, dsave) + # for i in range(int(1e6)): + # if task != 'F': + # logging.info('search converged in {} iterations'.format(i)) + # break + # fx = f(x, dfx) + # x, fx, dfx, task = minpack2.dsrch(x, fx, dfx, f_tol, df_tol, x_tol, task, + # x_min, x_max, isave, dsave) + + # return x, fx, dfx + From c4cbdff751c19716b788c582ac6d4df0af5d3576 Mon Sep 17 00:00:00 2001 From: jan Date: Mon, 8 Jan 2018 23:28:57 -0800 Subject: [PATCH 02/77] cleanup --- fdfd_tools/bloch.py | 125 ++++++++++++++++++++++---------------------- 1 file changed, 63 insertions(+), 62 deletions(-) diff --git a/fdfd_tools/bloch.py b/fdfd_tools/bloch.py index 31d364d..de070c0 100644 --- a/fdfd_tools/bloch.py +++ b/fdfd_tools/bloch.py @@ -447,15 +447,10 @@ def eigsolve(num_modes: int, continue break - def rtrace_AtB(A, B): - return real(numpy.sum(A.conj() * B)) - - def symmetrize(A): - return (A + A.conj().T) * 0.5 - max_iters = 10000 for iter in range(max_iters): - U = numpy.linalg.inv(Z.conj().T @ Z) + ZtZ = Z.conj().T @ Z + U = numpy.linalg.inv(ZtZ) AZ = scipy_op @ Z AZU = AZ @ U ZtAZU = Z.conj().T @ AZU @@ -469,47 +464,44 @@ def eigsolve(num_modes: int, break KG = scipy_iop @ G - traceGtKG = rtrace_AtB(G, KG) - gamma_numerator = traceGtKG + traceGtKG = _rtrace_AtB(G, KG) - reset_iters = 100 + reset_iters = 100 # TODO if prev_traceGtKG == 0 or iter % reset_iters == 0: - print('RESET!') + logger.inf('CG reset') gamma = 0 else: - gamma = gamma_numerator / prev_traceGtKG + gamma = traceGtKG / prev_traceGtKG D = gamma * d_scale * D + KG - d_scale = numpy.sqrt(rtrace_AtB(D, D)) / num_modes + d_scale = numpy.sqrt(_rtrace_AtB(D, D)) / num_modes D /= d_scale + ZtAZ = Z.conj().T @ AZ + AD = scipy_op @ D DtD = D.conj().T @ D DtAD = D.conj().T @ AD - ZtD = Z.conj().T @ D - ZtAD = Z.conj().T @ AD - symZtD = symmetrize(ZtD) - symZtAD = symmetrize(ZtAD) + symZtD = _symmetrize(Z.conj().T @ D) + symZtAD = _symmetrize(Z.conj().T @ AD) + ''' U_sZtD = U @ symZtD - dE = 2.0 * (rtrace_AtB(U, symZtAD) - rtrace_AtB(ZtAZU, U_sZtD)) + dE = 2.0 * (_rtrace_AtB(U, symZtAD) - + _rtrace_AtB(ZtAZU, U_sZtD)) - S2 = DtD - 4 * symZtD @ U_sZtD - d2E = 2 * (rtrace_AtB(U, DtAD) - - rtrace_AtB(ZtAZU, U @ S2) - - 4 * rtrace_AtB(U, symZtAD @ U_sZtD)) + d2E = 2 * (_rtrace_AtB(U, DtAD) - + _rtrace_AtB(ZtAZU, U @ (DtD - 4 * symZtD @ U_sZtD)) - + 4 * _rtrace_AtB(U, symZtAD @ U_sZtD)) # Newton-Raphson to find a root of the first derivative: theta = -dE/d2E if d2E < 0 or abs(theta) >= pi: theta = -abs(prev_theta) * numpy.sign(dE) - - # ZtAZU * ZtZ = ZtAZ for use in line search - ZtZ = Z.conj().T @ Z - ZtAZ = ZtAZU @ ZtZ.conj().T + ''' def Qi_func(theta, memo=[None, None]): if memo[0] == theta: @@ -525,10 +517,10 @@ def eigsolve(num_modes: int, # if c or s small, taylor expand if c < 1e-4 * s and c != 0: Qi = numpy.linalg.inv(DtD) - Qi = Qi / (s*s) - 2*c/(s*s*s) * (Qi @ symZtD.conj().T @ Qi.conj().T) + Qi = Qi / (s*s) - 2*c/(s*s*s) * (Qi @ (Qi @ symZtD).conj().T) elif s < 1e-4 * c and s != 0: Qi = numpy.linalg.inv(ZtZ) - Qi = Qi / (c*c) - 2*s/(c*c*c) * (Qi @ symZtD.conj().T @ Qi.conj().T) + Qi = Qi / (c*c) - 2*s/(c*c*c) * (Qi @ (Qi @ symZtD).conj().T) else: raise Exception('Inexplicable singularity in trace_func') memo[0] = theta @@ -540,22 +532,24 @@ def eigsolve(num_modes: int, s = numpy.sin(theta) Qi = Qi_func(theta) R = c*c * ZtAZ + s*s * DtAD + 2*s*c * symZtAD - trace = rtrace_AtB(R, Qi) + trace = _rtrace_AtB(R, Qi) return numpy.abs(trace) - #def trace_deriv(theta): - # Qi = Qi_func(theta) - # c2 = numpy.cos(2 * theta) - # s2 = numpy.sin(2 * theta) - # F = -0.5*s2 * (ZtAZ - DtAD) + c2 * symZtAD - # trace_deriv = rtrace_AtB(Qi, F) + ''' + def trace_deriv(theta): + Qi = Qi_func(theta) + c2 = numpy.cos(2 * theta) + s2 = numpy.sin(2 * theta) + F = -0.5*s2 * (ZtAZ - DtAD) + c2 * symZtAD + trace_deriv = _rtrace_AtB(Qi, F) - # G = Qi @ F.conj().T @ Qi.conj().T - # H = -0.5*s2 * (ZtZ - DtD) + c2 * symZtD - # trace_deriv -= rtrace_AtB(G, H) + G = Qi @ F.conj().T @ Qi.conj().T + H = -0.5*s2 * (ZtZ - DtD) + c2 * symZtD + trace_deriv -= _rtrace_AtB(G, H) - # trace_deriv *= 2 - # return trace_deriv * sgn + trace_deriv *= 2 + return trace_deriv * sgn + ''' ''' theta, new_E, new_dE = linmin(theta, E, dE, 0.1, min(tolerance, 1e-6), 1e-14, 0, -numpy.sign(dE) * K_PI, trace_func) @@ -597,29 +591,36 @@ def eigsolve(num_modes: int, order = numpy.argsort(numpy.abs(eigvals)) return eigvals[order], eigvecs.T[order] - #def linmin(x_guess, f0, df0, x_max, f_tol=0.1, df_tol=min(tolerance, 1e-6), x_tol=1e-14, x_min=0, linmin_func): - # if df0 > 0: - # x0, f0, df0 = linmin(-x_guess, f0, -df0, -x_max, f_tol, df_tol, x_tol, -x_min, lambda q, dq: -linmin_func(q, dq)) - # return -x0, f0, -df0 - # elif df0 == 0: - # return 0, f0, df0 - # else: - # x = x_guess - # fx = f0 - # dfx = df0 +#def linmin(x_guess, f0, df0, x_max, f_tol=0.1, df_tol=min(tolerance, 1e-6), x_tol=1e-14, x_min=0, linmin_func): +# if df0 > 0: +# x0, f0, df0 = linmin(-x_guess, f0, -df0, -x_max, f_tol, df_tol, x_tol, -x_min, lambda q, dq: -linmin_func(q, dq)) +# return -x0, f0, -df0 +# elif df0 == 0: +# return 0, f0, df0 +# else: +# x = x_guess +# fx = f0 +# dfx = df0 - # isave = numpy.zeros((2,), numpy.intc) - # dsave = numpy.zeros((13,), float) +# isave = numpy.zeros((2,), numpy.intc) +# dsave = numpy.zeros((13,), float) - # x, fx, dfx, task = minpack2.dsrch(x, fx, dfx, f_tol, df_tol, x_tol, task, - # x_min, x_max, isave, dsave) - # for i in range(int(1e6)): - # if task != 'F': - # logging.info('search converged in {} iterations'.format(i)) - # break - # fx = f(x, dfx) - # x, fx, dfx, task = minpack2.dsrch(x, fx, dfx, f_tol, df_tol, x_tol, task, - # x_min, x_max, isave, dsave) +# x, fx, dfx, task = minpack2.dsrch(x, fx, dfx, f_tol, df_tol, x_tol, task, +# x_min, x_max, isave, dsave) +# for i in range(int(1e6)): +# if task != 'F': +# logging.info('search converged in {} iterations'.format(i)) +# break +# fx = f(x, dfx) +# x, fx, dfx, task = minpack2.dsrch(x, fx, dfx, f_tol, df_tol, x_tol, task, +# x_min, x_max, isave, dsave) - # return x, fx, dfx +# return x, fx, dfx + + +def _rtrace_AtB(A, B): + return real(numpy.sum(A.conj() * B)) + +def _symmetrize(A): + return (A + A.conj().T) * 0.5 From e02040c7093de5894ad1857a7fae5569d1bfcaee Mon Sep 17 00:00:00 2001 From: jan Date: Mon, 8 Jan 2018 23:33:22 -0800 Subject: [PATCH 03/77] fixes and clarification --- fdfd_tools/bloch.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/fdfd_tools/bloch.py b/fdfd_tools/bloch.py index de070c0..ea9e760 100644 --- a/fdfd_tools/bloch.py +++ b/fdfd_tools/bloch.py @@ -385,7 +385,9 @@ def eigsolve(num_modes: int, G_matrix: numpy.ndarray, epsilon: field_t, mu: field_t = None, - tolerance = 1e-20, + tolerance: float = 1e-20, + max_iters: int = 10000, + reset_iters: int = 100, ) -> Tuple[numpy.ndarray, numpy.ndarray]: """ Find the first (lowest-frequency) num_modes eigenmodes with Bloch wavevector @@ -447,16 +449,15 @@ def eigsolve(num_modes: int, continue break - max_iters = 10000 for iter in range(max_iters): ZtZ = Z.conj().T @ Z U = numpy.linalg.inv(ZtZ) AZ = scipy_op @ Z AZU = AZ @ U ZtAZU = Z.conj().T @ AZU - E = real(trace(ZtAZU)) - sgn = numpy.sign(E) - E = numpy.abs(E) + E_signed = real(trace(ZtAZU)) + sgn = numpy.sign(E_signed) + E = numpy.abs(E_signed) G = (AZU - Z @ U @ ZtAZU) * sgn if iter > 0 and abs(E - prev_E) < tolerance * 0.5 * (E + prev_E + 1e-7): @@ -466,9 +467,8 @@ def eigsolve(num_modes: int, KG = scipy_iop @ G traceGtKG = _rtrace_AtB(G, KG) - reset_iters = 100 # TODO if prev_traceGtKG == 0 or iter % reset_iters == 0: - logger.inf('CG reset') + logger.info('CG reset') gamma = 0 else: gamma = traceGtKG / prev_traceGtKG From 0e47fdd5fb97da65bc211b83ce1c3f8cabf3b985 Mon Sep 17 00:00:00 2001 From: jan Date: Tue, 9 Jan 2018 00:00:58 -0800 Subject: [PATCH 04/77] randomize imaginary part of starting vector --- fdfd_tools/bloch.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/fdfd_tools/bloch.py b/fdfd_tools/bloch.py index ea9e760..168a349 100644 --- a/fdfd_tools/bloch.py +++ b/fdfd_tools/bloch.py @@ -422,24 +422,24 @@ def eigsolve(num_modes: int, prev_E = 0 d_scale = 1 prev_traceGtKG = 0 - prev_theta = 0.5 + #prev_theta = 0.5 D = numpy.zeros(shape=y_shape, dtype=complex) y0 = None if y0 is None: - Z = numpy.random.rand(*y_shape).astype(complex) + Z = numpy.random.rand(*y_shape) + 1j * numpy.random.rand(*y_shape) else: Z = y0 while True: - Z2 = Z.conj().T @ Z - Z_norm = numpy.sqrt(real(trace(Z2))) / num_modes + ZtZ = Z.conj().T @ Z + Z_norm = numpy.sqrt(real(trace(ZtZ))) / num_modes Z /= Z_norm - Z2 /= Z_norm * Z_norm + ZtZ /= Z_norm * Z_norm try: - U = numpy.linalg.inv(Z2) + U = numpy.linalg.inv(ZtZ) except numpy.linalg.LinAlgError: - Z = numpy.random.rand(*y_shape).astype(complex) + Z = numpy.random.rand(*y_shape) + 1j * numpy.random.rand(*y_shape) continue trace_U = real(trace(U)) @@ -565,7 +565,7 @@ def eigsolve(num_modes: int, Z += D * numpy.sin(theta) prev_traceGtKG = traceGtKG - prev_theta = theta + #prev_theta = theta prev_E = E ''' From e8f836c908996fa80f866174f3ff7a1dc7ebb8db Mon Sep 17 00:00:00 2001 From: jan Date: Mon, 15 Jan 2018 22:43:33 -0800 Subject: [PATCH 05/77] Cleanup --- fdfd_tools/bloch.py | 106 +++++++++++++++++++++----------------------- 1 file changed, 50 insertions(+), 56 deletions(-) diff --git a/fdfd_tools/bloch.py b/fdfd_tools/bloch.py index 168a349..1816bdc 100644 --- a/fdfd_tools/bloch.py +++ b/fdfd_tools/bloch.py @@ -73,7 +73,7 @@ This module contains functions for generating and solving the ''' -from typing import List, Tuple, Callable, Dict +from typing import Tuple, Callable import logging import numpy from numpy import pi, real, trace @@ -83,7 +83,6 @@ import scipy.optimize from scipy.linalg import norm import scipy.sparse.linalg as spalg -from .eigensolvers import rayleigh_quotient_iteration from . import field_t logger = logging.getLogger(__name__) @@ -256,7 +255,7 @@ def hmn_2_hxyz(k0: numpy.ndarray, :return: Function for converting h_mn into H_xyz """ shape = epsilon[0].shape + (1,) - k_mag, m, n = generate_kmn(k0, G_matrix, shape) + _k_mag, m, n = generate_kmn(k0, G_matrix, shape) def operator(h: numpy.ndarray): hin_m, hin_n = [hi.reshape(shape) for hi in numpy.split(h, 2)] @@ -379,7 +378,6 @@ def find_k(frequency: float, return res.x * direction, res.fun + frequency - def eigsolve(num_modes: int, k0: numpy.ndarray, G_matrix: numpy.ndarray, @@ -432,10 +430,8 @@ def eigsolve(num_modes: int, Z = y0 while True: + Z *= num_modes / norm(Z) ZtZ = Z.conj().T @ Z - Z_norm = numpy.sqrt(real(trace(ZtZ))) / num_modes - Z /= Z_norm - ZtZ /= Z_norm * Z_norm try: U = numpy.linalg.inv(ZtZ) except numpy.linalg.LinAlgError: @@ -449,7 +445,7 @@ def eigsolve(num_modes: int, continue break - for iter in range(max_iters): + for i in range(max_iters): ZtZ = Z.conj().T @ Z U = numpy.linalg.inv(ZtZ) AZ = scipy_op @ Z @@ -460,22 +456,22 @@ def eigsolve(num_modes: int, E = numpy.abs(E_signed) G = (AZU - Z @ U @ ZtAZU) * sgn - if iter > 0 and abs(E - prev_E) < tolerance * 0.5 * (E + prev_E + 1e-7): + if i > 0 and abs(E - prev_E) < tolerance * 0.5 * (E + prev_E + 1e-7): logging.info('Optimization succeded: {} - 5e-8 < {} * {} / 2'.format(abs(E - prev_E), tolerance, E + prev_E)) break KG = scipy_iop @ G traceGtKG = _rtrace_AtB(G, KG) - if prev_traceGtKG == 0 or iter % reset_iters == 0: + if prev_traceGtKG == 0 or i % reset_iters == 0: logger.info('CG reset') gamma = 0 else: gamma = traceGtKG / prev_traceGtKG - D = gamma * d_scale * D + KG - d_scale = numpy.sqrt(_rtrace_AtB(D, D)) / num_modes - D /= d_scale + D = gamma / d_scale * D + KG + d_scale = num_modes / norm(D) + D *= d_scale ZtAZ = Z.conj().T @ AZ @@ -486,22 +482,6 @@ def eigsolve(num_modes: int, symZtD = _symmetrize(Z.conj().T @ D) symZtAD = _symmetrize(Z.conj().T @ AD) - ''' - U_sZtD = U @ symZtD - - dE = 2.0 * (_rtrace_AtB(U, symZtAD) - - _rtrace_AtB(ZtAZU, U_sZtD)) - - d2E = 2 * (_rtrace_AtB(U, DtAD) - - _rtrace_AtB(ZtAZU, U @ (DtD - 4 * symZtD @ U_sZtD)) - - 4 * _rtrace_AtB(U, symZtAD @ U_sZtD)) - - # Newton-Raphson to find a root of the first derivative: - theta = -dE/d2E - - if d2E < 0 or abs(theta) >= pi: - theta = -abs(prev_theta) * numpy.sign(dE) - ''' def Qi_func(theta, memo=[None, None]): if memo[0] == theta: @@ -549,12 +529,25 @@ def eigsolve(num_modes: int, trace_deriv *= 2 return trace_deriv * sgn - ''' + U_sZtD = U @ symZtD + + dE = 2.0 * (_rtrace_AtB(U, symZtAD) - + _rtrace_AtB(ZtAZU, U_sZtD)) + + d2E = 2 * (_rtrace_AtB(U, DtAD) - + _rtrace_AtB(ZtAZU, U @ (DtD - 4 * symZtD @ U_sZtD)) - + 4 * _rtrace_AtB(U, symZtAD @ U_sZtD)) + + # Newton-Raphson to find a root of the first derivative: + theta = -dE/d2E + + if d2E < 0 or abs(theta) >= pi: + theta = -abs(prev_theta) * numpy.sign(dE) + + # theta, new_E, new_dE = linmin(theta, E, dE, 0.1, min(tolerance, 1e-6), 1e-14, 0, -numpy.sign(dE) * K_PI, trace_func) + theta, n, _, new_E, _, _new_dE = scipy.optimize.line_search(trace_func, trace_deriv, xk=theta, pk=numpy.ones((1,1)), gfk=dE, old_fval=E, c1=min(tolerance, 1e-6), c2=0.1, amax=pi) ''' - theta, new_E, new_dE = linmin(theta, E, dE, 0.1, min(tolerance, 1e-6), 1e-14, 0, -numpy.sign(dE) * K_PI, trace_func) - ''' - #theta, n, _, new_E, _, _new_dE = scipy.optimize.line_search(trace_func, trace_deriv, xk=theta, pk=numpy.ones((1,1)), gfk=dE, old_fval=E, c1=min(tolerance, 1e-6), c2=0.1, amax=pi) result = scipy.optimize.minimize_scalar(trace_func, bounds=(0, pi), tol=tolerance) new_E = result.fun theta = result.x @@ -591,32 +584,33 @@ def eigsolve(num_modes: int, order = numpy.argsort(numpy.abs(eigvals)) return eigvals[order], eigvecs.T[order] -#def linmin(x_guess, f0, df0, x_max, f_tol=0.1, df_tol=min(tolerance, 1e-6), x_tol=1e-14, x_min=0, linmin_func): -# if df0 > 0: -# x0, f0, df0 = linmin(-x_guess, f0, -df0, -x_max, f_tol, df_tol, x_tol, -x_min, lambda q, dq: -linmin_func(q, dq)) -# return -x0, f0, -df0 -# elif df0 == 0: -# return 0, f0, df0 -# else: -# x = x_guess -# fx = f0 -# dfx = df0 +''' +def linmin(x_guess, f0, df0, x_max, f_tol=0.1, df_tol=min(tolerance, 1e-6), x_tol=1e-14, x_min=0, linmin_func): + if df0 > 0: + x0, f0, df0 = linmin(-x_guess, f0, -df0, -x_max, f_tol, df_tol, x_tol, -x_min, lambda q, dq: -linmin_func(q, dq)) + return -x0, f0, -df0 + elif df0 == 0: + return 0, f0, df0 + else: + x = x_guess + fx = f0 + dfx = df0 -# isave = numpy.zeros((2,), numpy.intc) -# dsave = numpy.zeros((13,), float) + isave = numpy.zeros((2,), numpy.intc) + dsave = numpy.zeros((13,), float) -# x, fx, dfx, task = minpack2.dsrch(x, fx, dfx, f_tol, df_tol, x_tol, task, -# x_min, x_max, isave, dsave) -# for i in range(int(1e6)): -# if task != 'F': -# logging.info('search converged in {} iterations'.format(i)) -# break -# fx = f(x, dfx) -# x, fx, dfx, task = minpack2.dsrch(x, fx, dfx, f_tol, df_tol, x_tol, task, -# x_min, x_max, isave, dsave) - -# return x, fx, dfx + x, fx, dfx, task = minpack2.dsrch(x, fx, dfx, f_tol, df_tol, x_tol, task, + x_min, x_max, isave, dsave) + for i in range(int(1e6)): + if task != 'F': + logging.info('search converged in {} iterations'.format(i)) + break + fx = f(x, dfx) + x, fx, dfx, task = minpack2.dsrch(x, fx, dfx, f_tol, df_tol, x_tol, task, + x_min, x_max, isave, dsave) + return x, fx, dfx +''' def _rtrace_AtB(A, B): return real(numpy.sum(A.conj() * B)) From c1f65f61c1db4d45a5d2df0233b196aa028a23e1 Mon Sep 17 00:00:00 2001 From: jan Date: Mon, 15 Jan 2018 22:43:59 -0800 Subject: [PATCH 06/77] Use pyfftw if available --- fdfd_tools/bloch.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/fdfd_tools/bloch.py b/fdfd_tools/bloch.py index 1816bdc..fd5f758 100644 --- a/fdfd_tools/bloch.py +++ b/fdfd_tools/bloch.py @@ -77,7 +77,7 @@ from typing import Tuple, Callable import logging import numpy from numpy import pi, real, trace -from numpy.fft import fftn, ifftn, fftfreq +from numpy.fft import fftfreq import scipy import scipy.optimize from scipy.linalg import norm @@ -88,6 +88,29 @@ from . import field_t logger = logging.getLogger(__name__) +try: + import pyfftw.interfaces.numpy_fft + import pyfftw.interfaces + import multiprocessing + + pyfftw.interfaces.cache.enable() + pyfftw.interfaces.cache.set_keepalive_time(3600) + fftw_args = { + 'threads': multiprocessing.cpu_count(), + 'overwrite_input': True, + 'planner_effort': 'FFTW_EXHAUSTIVE', + } + + def fftn(*args, **kwargs): + return pyfftw.interfaces.numpy_fft.fftn(*args, **kwargs, **fftw_args) + + def ifftn(*args, **kwargs): + return pyfftw.interfaces.numpy_fft.ifftn(*args, **kwargs, **fftw_args) + +except ImportError: + from numpy.fft import fftn, ifftn + + def generate_kmn(k0: numpy.ndarray, G_matrix: numpy.ndarray, shape: numpy.ndarray From ee9abb77d9f42e79074357e95aee960e7588a566 Mon Sep 17 00:00:00 2001 From: jan Date: Mon, 15 Jan 2018 22:44:14 -0800 Subject: [PATCH 07/77] Fix approx_inverse operator --- fdfd_tools/bloch.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fdfd_tools/bloch.py b/fdfd_tools/bloch.py index fd5f758..aabc96c 100644 --- a/fdfd_tools/bloch.py +++ b/fdfd_tools/bloch.py @@ -352,8 +352,8 @@ def inverse_maxwell_operator_approx(k0: numpy.ndarray, d_xyz = fftn(ifftn(e_xyz, axes=range(3)) * epsilon, axes=range(3)) # cross product and transform into mn basis crossinv_t2c - h_m = numpy.sum(e_xyz * n, axis=3)[:, :, :, None] / +k_mag - h_n = numpy.sum(e_xyz * m, axis=3)[:, :, :, None] / -k_mag + h_m = numpy.sum(d_xyz * n, axis=3)[:, :, :, None] / +k_mag + h_n = numpy.sum(d_xyz * m, axis=3)[:, :, :, None] / -k_mag return numpy.hstack((h_m.ravel(), h_n.ravel())) From 323bcf88ad224f5a77df7f4e3d4c25e46055ffcf Mon Sep 17 00:00:00 2001 From: jan Date: Mon, 15 Jan 2018 22:44:26 -0800 Subject: [PATCH 08/77] Propagate mu correctly --- fdfd_tools/bloch.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fdfd_tools/bloch.py b/fdfd_tools/bloch.py index aabc96c..cbfdf1c 100644 --- a/fdfd_tools/bloch.py +++ b/fdfd_tools/bloch.py @@ -389,7 +389,7 @@ def find_k(frequency: float, def get_f(k0_mag: float, band: int = 0): k0 = direction * k0_mag - n, _v = eigsolve(band + 1, k0, G_matrix=G_matrix, epsilon=epsilon) + n, _v = eigsolve(band + 1, k0, G_matrix=G_matrix, epsilon=epsilon, mu=mu) f = numpy.sqrt(numpy.abs(numpy.real(n[band]))) return f From 1f9a9949c06ff33a836b05e2b18f18b1495e39fc Mon Sep 17 00:00:00 2001 From: jan Date: Mon, 15 Jan 2018 22:44:59 -0800 Subject: [PATCH 09/77] Clarify memo and cleanup --- fdfd_tools/bloch.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/fdfd_tools/bloch.py b/fdfd_tools/bloch.py index cbfdf1c..002234f 100644 --- a/fdfd_tools/bloch.py +++ b/fdfd_tools/bloch.py @@ -505,10 +505,11 @@ def eigsolve(num_modes: int, symZtD = _symmetrize(Z.conj().T @ D) symZtAD = _symmetrize(Z.conj().T @ AD) - - def Qi_func(theta, memo=[None, None]): - if memo[0] == theta: - return memo[1] + Qi_memo = [None, None] + def Qi_func(theta): + nonlocal Qi_memo + if Qi_memo[0] == theta: + return Qi_memo[1] c = numpy.cos(theta) s = numpy.sin(theta) @@ -519,15 +520,15 @@ def eigsolve(num_modes: int, logger.info('taylor Qi') # if c or s small, taylor expand if c < 1e-4 * s and c != 0: - Qi = numpy.linalg.inv(DtD) - Qi = Qi / (s*s) - 2*c/(s*s*s) * (Qi @ (Qi @ symZtD).conj().T) + DtDi = numpy.linalg.inv(DtD) + Qi = DtDi / (s*s) - 2*c/(s*s*s) * (DtDi @ (DtDi @ symZtD).conj().T) elif s < 1e-4 * c and s != 0: - Qi = numpy.linalg.inv(ZtZ) - Qi = Qi / (c*c) - 2*s/(c*c*c) * (Qi @ (Qi @ symZtD).conj().T) + ZtZi = numpy.linalg.inv(ZtZ) + Qi = ZtZi / (c*c) - 2*s/(c*c*c) * (ZtZi @ (ZtZi @ symZtD).conj().T) else: raise Exception('Inexplicable singularity in trace_func') - memo[0] = theta - memo[1] = Qi + Qi_memo[0] = theta + Qi_memo[1] = Qi return Qi def trace_func(theta): From c7d4c4a8e6096aa79acc2f9c3003ccd02340224a Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 9 Jul 2019 20:07:13 -0700 Subject: [PATCH 10/77] Add callback for block mode solve progress --- fdfd_tools/bloch.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/fdfd_tools/bloch.py b/fdfd_tools/bloch.py index 002234f..598aa5d 100644 --- a/fdfd_tools/bloch.py +++ b/fdfd_tools/bloch.py @@ -369,6 +369,7 @@ def find_k(frequency: float, band: int = 0, k_min: float = 0, k_max: float = 0.5, + solve_callback: Callable = None ) -> Tuple[numpy.ndarray, float]: """ Search for a bloch vector that has a given frequency. @@ -389,8 +390,10 @@ def find_k(frequency: float, def get_f(k0_mag: float, band: int = 0): k0 = direction * k0_mag - n, _v = eigsolve(band + 1, k0, G_matrix=G_matrix, epsilon=epsilon, mu=mu) + n, v = eigsolve(band + 1, k0, G_matrix=G_matrix, epsilon=epsilon, mu=mu) f = numpy.sqrt(numpy.abs(numpy.real(n[band]))) + if solve_callback: + solve_callback(k0_mag, n, v, f) return f res = scipy.optimize.minimize_scalar(lambda x: abs(get_f(x, band) - frequency), From 41cd94fe48fdd1521c1e833e49e43e916e07bd82 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 9 Jul 2019 20:07:44 -0700 Subject: [PATCH 11/77] More detailed logging --- fdfd_tools/bloch.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/fdfd_tools/bloch.py b/fdfd_tools/bloch.py index 598aa5d..9c252e7 100644 --- a/fdfd_tools/bloch.py +++ b/fdfd_tools/bloch.py @@ -92,6 +92,7 @@ try: import pyfftw.interfaces.numpy_fft import pyfftw.interfaces import multiprocessing + logger.info('Using pyfftw') pyfftw.interfaces.cache.enable() pyfftw.interfaces.cache.set_keepalive_time(3600) @@ -109,6 +110,7 @@ try: except ImportError: from numpy.fft import fftn, ifftn + logger.info('Using numpy fft') def generate_kmn(k0: numpy.ndarray, @@ -483,7 +485,7 @@ def eigsolve(num_modes: int, G = (AZU - Z @ U @ ZtAZU) * sgn if i > 0 and abs(E - prev_E) < tolerance * 0.5 * (E + prev_E + 1e-7): - logging.info('Optimization succeded: {} - 5e-8 < {} * {} / 2'.format(abs(E - prev_E), tolerance, E + prev_E)) + logger.info('Optimization succeded: {} - 5e-8 < {} * {} / 2'.format(abs(E - prev_E), tolerance, E + prev_E)) break KG = scipy_iop @ G From 001c32a2e0b85c327e36c5bbb62b283c4ba11f10 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 9 Jul 2019 20:08:33 -0700 Subject: [PATCH 12/77] Partially fix arbitrary mode phase --- fdfd_tools/waveguide.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/fdfd_tools/waveguide.py b/fdfd_tools/waveguide.py index 1566a74..89e0d9c 100644 --- a/fdfd_tools/waveguide.py +++ b/fdfd_tools/waveguide.py @@ -112,9 +112,16 @@ def normalized_fields(v: numpy.ndarray, P = 0.5 * numpy.real(S.sum()) assert P > 0, 'Found a mode propagating in the wrong direction! P={}'.format(P) + energy = epsilon * e.conj() * e + norm_amplitude = 1 / numpy.sqrt(P) - norm_angle = -numpy.angle(e[e.size//2]) - norm_factor = norm_amplitude * numpy.exp(1j * norm_angle) + norm_angle = -numpy.angle(e[energy.argmax()]) # Will randomly add a negative sign when mode is symmetric + + # Try to break symmetry to assign a consistent sign [experimental] + E_weighted = unvec(e * energy * numpy.exp(1j * norm_angle), shape) + sign = numpy.sign(E_weighted[:, :max(shape[0]//2, 1), :max(shape[1]//2, 1)].real.sum()) + + norm_factor = sign * norm_amplitude * numpy.exp(1j * norm_angle) e *= norm_factor h *= norm_factor From c3f248a73c53adc2ffc24ebf677e3c61b93d673f Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 9 Jul 2019 20:08:44 -0700 Subject: [PATCH 13/77] Clarify beta=wavenumber --- fdfd_tools/waveguide_mode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fdfd_tools/waveguide_mode.py b/fdfd_tools/waveguide_mode.py index e50cc6a..b7a17a3 100644 --- a/fdfd_tools/waveguide_mode.py +++ b/fdfd_tools/waveguide_mode.py @@ -55,7 +55,7 @@ def solve_waveguide_mode_2d(mode_number: int, See Numerical Dispersion in Taflove's FDTD book. This correction term reduces the error in emitted power, but additional error is introduced into the E_err and H_err terms. This effect becomes - more pronounced as beta increases. + more pronounced as the wavenumber increases. ''' if wavenumber_correction: wavenumber -= 2 * numpy.sin(numpy.real(wavenumber / 2)) - numpy.real(wavenumber) From 5dd26915fc66ada63d64b628a08211dd47aed0b0 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 9 Jul 2019 20:09:12 -0700 Subject: [PATCH 14/77] wavenumber correction must take dx into account --- fdfd_tools/waveguide_mode.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fdfd_tools/waveguide_mode.py b/fdfd_tools/waveguide_mode.py index b7a17a3..96ab16a 100644 --- a/fdfd_tools/waveguide_mode.py +++ b/fdfd_tools/waveguide_mode.py @@ -58,7 +58,8 @@ def solve_waveguide_mode_2d(mode_number: int, more pronounced as the wavenumber increases. ''' if wavenumber_correction: - wavenumber -= 2 * numpy.sin(numpy.real(wavenumber / 2)) - numpy.real(wavenumber) + dx_mean = (numpy.hstack(dxes[0]) + numpy.hstack(dxes[1])).mean() / 2 #TODO figure out what dx to use here + wavenumber -= 2 * numpy.sin(numpy.real(wavenumber * dx_mean / 2)) / dx_mean - numpy.real(wavenumber) shape = [d.size for d in dxes[0]] fields = { From 2b3a74b737b448b8aaa6eef99c01a9d3cba90383 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 9 Jul 2019 20:11:32 -0700 Subject: [PATCH 15/77] Fix waveguide source computation for different polarities etc. --- fdfd_tools/waveguide_mode.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/fdfd_tools/waveguide_mode.py b/fdfd_tools/waveguide_mode.py index 96ab16a..917c535 100644 --- a/fdfd_tools/waveguide_mode.py +++ b/fdfd_tools/waveguide_mode.py @@ -101,6 +101,8 @@ def solve_waveguide_mode(mode_number: int, if mu is None: mu = [numpy.ones_like(epsilon[0])] * 3 + slices = tuple(slices) + ''' Solve the 2D problem in the specified plane ''' @@ -183,23 +185,23 @@ def compute_source(E: field_t, J = [None]*3 M = [None]*3 - src_order = numpy.roll(range(3), axis) + src_order = numpy.roll(range(3), -axis) exp_iphi = numpy.exp(1j * polarity * wavenumber * dxes[1][axis][slices[axis]]) J[src_order[0]] = numpy.zeros_like(E[0]) J[src_order[1]] = +exp_iphi * H[src_order[2]] * polarity J[src_order[2]] = -exp_iphi * H[src_order[1]] * polarity + rollby = -1 if polarity > 0 else 0 M[src_order[0]] = numpy.zeros_like(E[0]) - M[src_order[1]] = +numpy.roll(E[src_order[2]], -1, axis=axis) - M[src_order[2]] = -numpy.roll(E[src_order[1]], -1, axis=axis) + M[src_order[1]] = +numpy.roll(E[src_order[2]], rollby, axis=axis) + M[src_order[2]] = -numpy.roll(E[src_order[1]], rollby, axis=axis) - A1f = functional.curl_h(dxes) + m2j = functional.m2j(omega, dxes, mu) + Jm = m2j(M) - Jm_iw = A1f([M[k] / mu[k] for k in range(3)]) - for k in range(3): - J[k] += Jm_iw[k] / (-1j * omega) + Jtot = [ji + jmi for ji, jmi in zip(J, Jm)] - return J + return Jtot def compute_overlap_e(E: field_t, From 3a5d75cde4155cf615ffb8058eddf99264d13271 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 9 Jul 2019 20:11:45 -0700 Subject: [PATCH 16/77] fix typo --- fdfd_tools/operators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fdfd_tools/operators.py b/fdfd_tools/operators.py index 02c1197..8809d09 100644 --- a/fdfd_tools/operators.py +++ b/fdfd_tools/operators.py @@ -451,7 +451,7 @@ def avgb(axis: int, shape: List[int]) -> sparse.spmatrix: def poynting_e_cross(e: vfield_t, dxes: dx_lists_t) -> sparse.spmatrix: """ - Operator for computing the Poynting vector, contining the (E x) portion of the Poynting vector. + Operator for computing the Poynting vector, containing the (E x) portion of the Poynting vector. :param e: Vectorized E-field for the ExH cross product :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header From 2acbda4764483253863cf4bedf3eadb1f4dadf8e Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 9 Jul 2019 20:12:03 -0700 Subject: [PATCH 17/77] Force slices to be a tuple --- fdfd_tools/waveguide_mode.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fdfd_tools/waveguide_mode.py b/fdfd_tools/waveguide_mode.py index 917c535..ab27d35 100644 --- a/fdfd_tools/waveguide_mode.py +++ b/fdfd_tools/waveguide_mode.py @@ -234,6 +234,8 @@ def compute_overlap_e(E: field_t, :param mu: Magnetic permeability (default 1 everywhere) :return: overlap_e for calculating the mode overlap """ + slices = tuple(slices) + cross_plane = [slice(None)] * 3 cross_plane[axis] = slices[axis] From d462ae94121b4fbeaaecf13444092137f7da1497 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 9 Jul 2019 20:12:48 -0700 Subject: [PATCH 18/77] unvec to (3, *shape) rather than list-of-ndarrays --- fdfd_tools/vectorization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fdfd_tools/vectorization.py b/fdfd_tools/vectorization.py index e7b9645..bdc9d6a 100644 --- a/fdfd_tools/vectorization.py +++ b/fdfd_tools/vectorization.py @@ -45,5 +45,5 @@ def unvec(v: vfield_t, shape: numpy.ndarray) -> field_t: """ if numpy.any(numpy.equal(v, None)): return None - return [vi.reshape(shape, order='C') for vi in numpy.split(v, 3)] + return vi.reshape((3, *shape), order='C') From 4c2035c88231ddfd7c3b4b0a25078996bb3df28f Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 9 Jul 2019 20:13:07 -0700 Subject: [PATCH 19/77] Add m2j() function --- fdfd_tools/functional.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/fdfd_tools/functional.py b/fdfd_tools/functional.py index f184223..1d39d84 100644 --- a/fdfd_tools/functional.py +++ b/fdfd_tools/functional.py @@ -147,3 +147,36 @@ def e2h(omega: complex, return e2h_1_1 else: return e2h_mu + + +def m2j(omega: complex, + dxes: dx_lists_t, + mu: field_t = None, + ) -> functional_matrix: + """ + Utility operator for converting magnetic current (M) distribution + into equivalent electric current distribution (J). + For use with e.g. e_full(). + + :param omega: Angular frequency of the simulation + :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header + :param mu: Magnetic permeability (default 1 everywhere) + :return: Function for converting M to J + """ + ch = curl_h(dxes) + + def m2j_mu(m): + m_mu = [m[k] / mu[k] for k in range[3]] + J = [Ji / (-1j * omega) for Ji in ch(m_mu)] + return J + + def m2j_1(m): + J = [Ji / (-1j * omega) for Ji in ch(m)] + return J + + if numpy.any(numpy.equal(mu, None)): + return m2j_1 + else: + return m2j_mu + + From 8e634e35df9d624786c92fd2e03b7b333cdf183c Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 9 Jul 2019 20:13:31 -0700 Subject: [PATCH 20/77] Add experimental source types --- fdfd_tools/waveguide_mode.py | 139 +++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) diff --git a/fdfd_tools/waveguide_mode.py b/fdfd_tools/waveguide_mode.py index ab27d35..aac718d 100644 --- a/fdfd_tools/waveguide_mode.py +++ b/fdfd_tools/waveguide_mode.py @@ -343,3 +343,142 @@ def solve_waveguide_mode_cylindrical(mode_number: int, } return fields + + +def compute_source_q(E: field_t, + H: field_t, + wavenumber: complex, + omega: complex, + dxes: dx_lists_t, + axis: int, + polarity: int, + slices: List[slice], + mu: field_t = None, + ) -> field_t: + A1f = functional.curl_h(dxes) + A2f = functional.curl_e(dxes) + + J = A1f(H) + M = A2f([-E[i] for i in range(3)]) + + m2j = functional.m2j(omega, dxes, mu) + Jm = m2j(M) + + Jtot = [ji + jmi for ji, jmi in zip(J, Jm)] + + return Jtot, J, M + + + +def compute_source_e(QE: field_t, + omega: complex, + dxes: dx_lists_t, + axis: int, + polarity: int, + slices: List[slice], + epsilon: field_t, + mu: field_t = None, + ) -> field_t: + """ + Want (AQ-QA) E = -iwj, where Q is a mask + If E is an eigenmode, AE = 0 so just AQE = -iwJ + Really only need E in 4 cells along axis (0, 0, Emode1, Emode2), find AE (1 fdtd step), then use center 2 cells as src + """ + slices = tuple(slices) + + # Trim a cell from each end of the propagation axis + slices_reduced = list(slices) + slices_reduced[axis] = slice(slices[axis].start + 1, slices[axis].stop - 1) + slices_reduced = tuple(slices) + + # Don't actually need to mask out E here since it needs to be pre-masked (QE) + + A = functional.e_full(omega, dxes, epsilon, mu) + J4 = [ji / (-1j * omega) for ji in A(QE)] #J4 is 4-cell result of -iwJ = A QE + + J = numpy.zeros_like(J4) + for a in range(3): + J[a][slices_reduced] = J4[a][slices_reduced] + return J + + +def compute_source_wg(E: field_t, + wavenumber: complex, + omega: complex, + dxes: dx_lists_t, + axis: int, + polarity: int, + slices: List[slice], + epsilon: field_t, + mu: field_t = None, + ) -> field_t: + slices = tuple(slices) + Etgt, _slices2 = compute_overlap_ce(E=E, wavenumber=wavenumber, + dxes=dxes, axis=axis, polarity=polarity, + slices=slices) + + slices4 = list(slices) + slices4[axis] = slice(slices[axis].start - 4 * polarity, slices[axis].start) + slices4 = tuple(slices4) + + J = compute_source_e(QE=Etgt, + omega=omega, dxes=dxes, axis=axis, + polarity=polarity, slices=slices4, + epsilon=epsilon, mu=mu) + +def compute_overlap_ce(E: field_t, + wavenumber: complex, + dxes: dx_lists_t, + axis: int, + polarity: int, + slices: List[slice], + ) -> field_t: + slices = tuple(slices) + + Ee = expand_wgmode_e(E=E, wavenumber=wavenumber, + dxes=dxes, axis=axis, polarity=polarity, + slices=slices) + + start, stop = sorted((slices[axis].start, slices[axis].start - 2 * polarity)) + + slices2 = list(slices) + slices2[axis] = slice(start, stop) + slices2 = tuple(slices2) + + Etgt = numpy.zeros_like(Ee) + for a in range(3): + Etgt[a][slices2] = Ee[a][slices2] + + Etgt /= (Etgt.conj() * Etgt).sum() + + return Etgt, slices2 + + +def expand_wgmode_e(E: field_t, + wavenumber: complex, + dxes: dx_lists_t, + axis: int, + polarity: int, + slices: List[slice], + ) -> field_t: + slices = tuple(slices) + + # Determine phase factors for parallel slices + a_shape = numpy.roll([1, -1, 1, 1], axis) + a_E = numpy.real(dxes[0][axis]).cumsum() + r_E = a_E - a_E[slices[axis]] + iphi = polarity * 1j * wavenumber + phase_E = numpy.exp(iphi * r_E).reshape(a_shape) + + # Expand our slice to the entire grid using the phase factors + Ee = numpy.zeros_like(E) + + slices_exp = list(slices) + slices_exp[axis] = slice(E[0].shape[axis]) + slices_exp = (slice(3), *slices_exp) + + Ee[slices_exp] = phase_E * numpy.array(E)[slices_Exp] + + return Ee + + From 9d1d8fe869125974cb134548e28d5f0ce77fcdc9 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 9 Jul 2019 20:13:49 -0700 Subject: [PATCH 21/77] Improve wisdom management --- examples/bloch.py | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/examples/bloch.py b/examples/bloch.py index 793bd89..fe1d6b1 100644 --- a/examples/bloch.py +++ b/examples/bloch.py @@ -2,11 +2,43 @@ import numpy, scipy, gridlock, fdfd_tools from fdfd_tools import bloch from numpy.linalg import norm import logging +from pathlib import Path logging.basicConfig(level=logging.DEBUG) logger = logging.getLogger(__name__) +WISDOM_FILEPATH = pathlib.Path.home() / '.local' / 'share' / 'pyfftw' / 'wisdom.pickle' + +def pyfftw_save_wisdom(path): + path = pathlib.Path(path) + try: + import pyfftw + import pickle + except ImportError as e: + pass + + path.parent.mkdir(parents=True, exist_ok=True) + with open(path, 'wb') as f: + pickle.dump(wisdom, f) + + +def pyfftw_load_wisdom(path): + path = pathlib.Path(path) + try: + import pyfftw + import pickle + except ImportError as e: + pass + + try: + with open(path, 'rb') as f: + wisdom = pickle.load(f) + pyfftw.import_wisdom(wisdom) + except FileNotFoundError as e: + pass + +logger.info('Drawing grid...') dx = 40 x_period = 400 y_period = z_period = 2000 @@ -32,6 +64,8 @@ g2.grids = [numpy.zeros(g.shape) for _ in range(6)] epsilon = [g.grids[0],] * 3 reciprocal_lattice = numpy.diag(1000/numpy.array([x_period, y_period, z_period])) #cols are vectors +pyfftw_load_wisdom(WISDOM_FILEPATH) + #print('Finding k at 1550nm') #k, f = bloch.find_k(frequency=1000/1550, # tolerance=(1000 * (1/1550 - 1/1551)), @@ -42,7 +76,7 @@ reciprocal_lattice = numpy.diag(1000/numpy.array([x_period, y_period, z_period]) # #print("k={}, f={}, 1/f={}, k/f={}".format(k, f, 1/f, norm(reciprocal_lattice @ k) / f )) -print('Finding f at [0.25, 0, 0]') +logger.info('Finding f at [0.25, 0, 0]') for k0x in [.25]: k0 = numpy.array([k0x, 0, 0]) @@ -66,3 +100,4 @@ for k0x in [.25]: n_eff = norm(reciprocal_lattice @ k0) / f print('kmag/f = n_eff = {} \n wl = {}\n'.format(n_eff, 1/f )) +pyfftw_save_wisdom(WISDOM_FILEPATH) From 557e7483569314a8dc4c2b0b0a8b15d8671f3558 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 9 Jul 2019 20:19:35 -0700 Subject: [PATCH 22/77] Reduce number of allocations during maxwell curls --- fdfd_tools/fdtd.py | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/fdfd_tools/fdtd.py b/fdfd_tools/fdtd.py index c4aa897..3687c82 100644 --- a/fdfd_tools/fdtd.py +++ b/fdfd_tools/fdtd.py @@ -26,10 +26,14 @@ def curl_h(dxes: dx_lists_t = None) -> functional_matrix: return f - numpy.roll(f, 1, axis=ax) def ch_fun(h: field_t) -> field_t: - e = [dh(h[2], 1) - dh(h[1], 2), - dh(h[0], 2) - dh(h[2], 0), - dh(h[1], 0) - dh(h[0], 1)] - return e + output = numpy.empty_like(h) + output[0] = dh(h[2], 1) + output[1] = dh(h[0], 2) + output[2] = dh(h[1], 0) + output[0] -= dh(h[1], 2) + output[1] -= dh(h[2], 0) + output[2] -= dh(h[0], 1) + return output return ch_fun @@ -51,10 +55,14 @@ def curl_e(dxes: dx_lists_t = None) -> functional_matrix: return numpy.roll(f, -1, axis=ax) - f def ce_fun(e: field_t) -> field_t: - h = [de(e[2], 1) - de(e[1], 2), - de(e[0], 2) - de(e[2], 0), - de(e[1], 0) - de(e[0], 1)] - return h + output = numpy.empty_like(e) + output[0] = de(e[2], 1) + output[1] = de(e[0], 2) + output[2] = de(e[1], 0) + output[0] -= de(e[1], 2) + output[1] -= de(e[2], 0) + output[2] -= de(e[0], 1) + return output return ce_fun From a8a5a69e1a747250f33f01fd4eb048287c9a969f Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 9 Jul 2019 20:20:05 -0700 Subject: [PATCH 23/77] Eliminate iterations over lists (assume ndarray instead of list of ndarrays) --- fdfd_tools/fdtd.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/fdfd_tools/fdtd.py b/fdfd_tools/fdtd.py index 3687c82..f72c7a3 100644 --- a/fdfd_tools/fdtd.py +++ b/fdfd_tools/fdtd.py @@ -71,9 +71,7 @@ def maxwell_e(dt: float, dxes: dx_lists_t = None) -> functional_matrix: curl_h_fun = curl_h(dxes) def me_fun(e: field_t, h: field_t, epsilon: field_t): - ch = curl_h_fun(h) - for ei, ci, epsi in zip(e, ch, epsilon): - ei += dt * ci / epsi + e += dt * curl_h_fun(h)/ epsilon return e return me_fun @@ -83,9 +81,7 @@ def maxwell_h(dt: float, dxes: dx_lists_t = None) -> functional_matrix: curl_e_fun = curl_e(dxes) def mh_fun(e: field_t, h: field_t): - ce = curl_e_fun(e) - for hi, ci in zip(h, ce): - hi -= dt * ci + h -= dt * curl_e_fun(e) return h return mh_fun From 099966f2910e6fb115cc029b7eb9bdc3da547109 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 9 Jul 2019 20:20:49 -0700 Subject: [PATCH 24/77] Add poynting vector and divergence --- fdfd_tools/fdtd.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/fdfd_tools/fdtd.py b/fdfd_tools/fdtd.py index f72c7a3..5279d22 100644 --- a/fdfd_tools/fdtd.py +++ b/fdfd_tools/fdtd.py @@ -241,3 +241,18 @@ def cpml(direction:int, return e, h return pml_e, pml_h, fields + + +def poynting(e, h): + s = [numpy.roll(e[1], -1, axis=0) * h[2] - numpy.roll(e[2], -1, axis=0) * h[1], + numpy.roll(e[2], -1, axis=1) * h[0] - numpy.roll(e[0], -1, axis=1) * h[2], + numpy.roll(e[0], -1, axis=2) * h[1] - numpy.roll(e[1], -1, axis=2) * h[0]] + return numpy.array(s) + + +def div_poyting(dt, dxes, e, h): + s = poynting(e, h) + ds = (s[0] - numpy.roll(s[0], 1, axis=0) + + s[1] - numpy.roll(s[1], 1, axis=1) + + s[2] - numpy.roll(s[2], 1, axis=2)) + return ds From ecaf9fa3d017c56ab7a12622b5d7b2f6cf5170c4 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Tue, 9 Jul 2019 20:21:14 -0700 Subject: [PATCH 25/77] Test code for cylindrical wg modesolver --- examples/tcyl.py | 92 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 examples/tcyl.py diff --git a/examples/tcyl.py b/examples/tcyl.py new file mode 100644 index 0000000..66caeb2 --- /dev/null +++ b/examples/tcyl.py @@ -0,0 +1,92 @@ +import importlib +import numpy +from numpy.linalg import norm + +from fdfd_tools import vec, unvec, waveguide_mode +import fdfd_tools +import fdfd_tools.functional +import fdfd_tools.grid +from fdfd_tools.solvers import generic as generic_solver + +import gridlock + +from matplotlib import pyplot + + +__author__ = 'Jan Petykiewicz' + + +def test1(solver=generic_solver): + dx = 20 # discretization (nm/cell) + pml_thickness = 10 # (number of cells) + + wl = 1550 # Excitation wavelength + omega = 2 * numpy.pi / wl + + # Device design parameters + w = 800 + th = 220 + center = [0, 0, 0] + r0 = 8e3 + + # refractive indices + n_wg = numpy.sqrt(12.6) # ~Si + n_air = 1.0 # air + + # Half-dimensions of the simulation grid + y_max = 1200 + z_max = 900 + xyz_max = numpy.array([800, y_max, z_max]) + (pml_thickness + 2) * dx + + # Coordinates of the edges of the cells. + half_edge_coords = [numpy.arange(dx/2, m + dx/2, step=dx) for m in xyz_max] + edge_coords = [numpy.hstack((-h[::-1], h)) for h in half_edge_coords] + edge_coords[0] = numpy.array([-dx, dx]) + + # #### Create the grid and draw the device #### + grid = gridlock.Grid(edge_coords, initial=n_air**2, num_grids=3) + grid.draw_cuboid(center=center, dimensions=[8e3, w, th], eps=n_wg**2) + + dxes = [grid.dxyz, grid.autoshifted_dxyz()] + for a in (1, 2): + for p in (-1, 1): + dxes = fdfd_tools.grid.stretch_with_scpml(dxes, omega=omega, axis=a, polarity=p, + thickness=pml_thickness) + + wg_args = { + 'omega': omega, + 'dxes': [(d[1], d[2]) for d in dxes], + 'epsilon': vec(g.transpose([1, 2, 0]) for g in grid.grids), + 'r0': r0, + } + + wg_results = waveguide_mode.solve_waveguide_mode_cylindrical(mode_number=0, **wg_args) + + E = wg_results['E'] + + n_eff = wl / (2 * numpy.pi / wg_results['wavenumber']) + print('n =', n_eff) + print('alpha (um^-1) =', -4 * numpy.pi * numpy.imag(n_eff) / (wl * 1e-3)) + + ''' + Plot results + ''' + def pcolor(v): + vmax = numpy.max(numpy.abs(v)) + pyplot.pcolor(v.T, cmap='seismic', vmin=-vmax, vmax=vmax) + pyplot.axis('equal') + pyplot.colorbar() + + pyplot.figure() + pyplot.subplot(2, 2, 1) + pcolor(numpy.real(E[0][:, :])) + pyplot.subplot(2, 2, 2) + pcolor(numpy.real(E[1][:, :])) + pyplot.subplot(2, 2, 3) + pcolor(numpy.real(E[2][:, :])) + pyplot.subplot(2, 2, 4) + pyplot.show() + + +if __name__ == '__main__': + test1() From dd4e6f294fb906fb05352923a6cacb757c5fd6ee Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 15 Jul 2019 01:21:12 -0700 Subject: [PATCH 26/77] update fdtd and add some fdtd tests --- fdfd_tools/fdtd.py | 139 +++++++++++++++++++++++++++++++--------- fdfd_tools/test_fdtd.py | 68 ++++++++++++++++++++ 2 files changed, 176 insertions(+), 31 deletions(-) create mode 100644 fdfd_tools/test_fdtd.py diff --git a/fdfd_tools/fdtd.py b/fdfd_tools/fdtd.py index 5279d22..0cdec0d 100644 --- a/fdfd_tools/fdtd.py +++ b/fdfd_tools/fdtd.py @@ -3,6 +3,8 @@ import numpy from . import dx_lists_t, field_t +#TODO fix pmls + __author__ = 'Jan Petykiewicz' @@ -71,7 +73,7 @@ def maxwell_e(dt: float, dxes: dx_lists_t = None) -> functional_matrix: curl_h_fun = curl_h(dxes) def me_fun(e: field_t, h: field_t, epsilon: field_t): - e += dt * curl_h_fun(h)/ epsilon + e += dt * curl_h_fun(h) / epsilon return e return me_fun @@ -150,7 +152,12 @@ def cpml(direction:int, dt: float, epsilon: field_t, thickness: int = 8, + ln_R_per_layer: float = -1.6, epsilon_eff: float = 1, + mu_eff: float = 1, + m: float = 3.5, + ma: float = 1, + cfs_alpha: float = 0, dtype: numpy.dtype = numpy.float32, ) -> Tuple[Callable, Callable, Dict[str, field_t]]: @@ -166,9 +173,9 @@ def cpml(direction:int, if epsilon_eff <= 0: raise Exception('epsilon_eff must be positive') - m = (3.5, 1) - sigma_max = 0.8 * (m[0] + 1) / numpy.sqrt(epsilon_eff) - alpha_max = 0 # TODO: Decide what to do about non-zero alpha + sigma_max = -ln_R_per_layer / 2 * (m + 1) + kappa_max = numpy.sqrt(epsilon_eff * mu_eff) + alpha_max = cfs_alpha transverse = numpy.delete(range(3), direction) u, v = transverse @@ -187,14 +194,17 @@ def cpml(direction:int, expand_slice[direction] = slice(None) def par(x): - sigma = ((x / thickness) ** m[0]) * sigma_max - alpha = ((1 - x / thickness) ** m[1]) * alpha_max - p0 = numpy.exp(-(sigma + alpha) * dt) - p1 = sigma / (sigma + alpha) * (p0 - 1) - return p0[expand_slice], p1[expand_slice] + scaling = (x / thickness) ** m + sigma = scaling * sigma_max + kappa = 1 + scaling * (kappa_max - 1) + alpha = ((1 - x / thickness) ** ma) * alpha_max + p0 = numpy.exp(-(sigma / kappa + alpha) * dt) + p1 = sigma / (sigma + kappa * alpha) * (p0 - 1) + p2 = 1 / kappa + return p0[expand_slice], p1[expand_slice], p2[expand_slice] - p0e, p1e = par(xe) - p0h, p1h = par(xh) + p0e, p1e, p2e = par(xe) + p0h, p1h, p2h = par(xh) region = [slice(None)] * 3 if polarity < 0: @@ -204,12 +214,9 @@ def cpml(direction:int, else: raise Exception('Bad polarity!') - if direction == 1: - se = 1 - else: - se = -1 + se = 1 if direction == 1 else -1 - # TODO check if epsilon is uniform? + # TODO check if epsilon is uniform in pml region? shape = list(epsilon[0].shape) shape[direction] = thickness psi_e = [numpy.zeros(shape, dtype=dtype), numpy.zeros(shape, dtype=dtype)] @@ -222,37 +229,107 @@ def cpml(direction:int, 'psi_h_v': psi_h[1], } + # Note that this is kinda slow -- would be faster to reuse dHv*p2h for the original + # H update, but then you have multiple arrays and a monolithic (field + pml) update operation def pml_e(e: field_t, h: field_t, epsilon: field_t) -> Tuple[field_t, field_t]: + dHv = h[v][region] - numpy.roll(h[v], 1, axis=direction)[region] + dHu = h[u][region] - numpy.roll(h[u], 1, axis=direction)[region] psi_e[0] *= p0e - psi_e[0] += p1e * (h[v][region] - numpy.roll(h[v], 1, axis=direction)[region]) + psi_e[0] += p1e * dHv * p2e psi_e[1] *= p0e - psi_e[1] += p1e * (h[u][region] - numpy.roll(h[u], 1, axis=direction)[region]) - e[u][region] += se * dt * psi_e[0] / epsilon[u][region] - e[v][region] -= se * dt * psi_e[1] / epsilon[v][region] + psi_e[1] += p1e * dHu * p2e + e[u][region] += se * dt / epsilon[u][region] * (psi_e[0] + (p2e - 1) * dHv) + e[v][region] -= se * dt / epsilon[v][region] * (psi_e[1] + (p2e - 1) * dHu) return e, h def pml_h(e: field_t, h: field_t) -> Tuple[field_t, field_t]: + dEv = (numpy.roll(e[v], -1, axis=direction)[region] - e[v][region]) + dEu = (numpy.roll(e[u], -1, axis=direction)[region] - e[u][region]) psi_h[0] *= p0h - psi_h[0] += p1h * (numpy.roll(e[v], -1, axis=direction)[region] - e[v][region]) + psi_h[0] += p1h * dEv * p2h psi_h[1] *= p0h - psi_h[1] += p1h * (numpy.roll(e[u], -1, axis=direction)[region] - e[u][region]) - h[u][region] -= se * dt * psi_h[0] - h[v][region] += se * dt * psi_h[1] + psi_h[1] += p1h * dEu * p2h + h[u][region] -= se * dt * (psi_h[0] + (p2h - 1) * dEv) + h[v][region] += se * dt * (psi_h[1] + (p2h - 1) * dEu) return e, h return pml_e, pml_h, fields def poynting(e, h): - s = [numpy.roll(e[1], -1, axis=0) * h[2] - numpy.roll(e[2], -1, axis=0) * h[1], + s = (numpy.roll(e[1], -1, axis=0) * h[2] - numpy.roll(e[2], -1, axis=0) * h[1], numpy.roll(e[2], -1, axis=1) * h[0] - numpy.roll(e[0], -1, axis=1) * h[2], - numpy.roll(e[0], -1, axis=2) * h[1] - numpy.roll(e[1], -1, axis=2) * h[0]] + numpy.roll(e[0], -1, axis=2) * h[1] - numpy.roll(e[1], -1, axis=2) * h[0]) return numpy.array(s) -def div_poyting(dt, dxes, e, h): - s = poynting(e, h) - ds = (s[0] - numpy.roll(s[0], 1, axis=0) + - s[1] - numpy.roll(s[1], 1, axis=1) + - s[2] - numpy.roll(s[2], 1, axis=2)) +def poynting_divergence(dt, dxes, s=None, *, e=None, h=None): # TODO dxes + if s is None: + s = poynting(e, h) + ds = ((s[0] - numpy.roll(s[0], 1, axis=0)) / numpy.sqrt(dxes[0][0] * dxes[1][0])[:, None, None] + + (s[1] - numpy.roll(s[1], 1, axis=1)) / numpy.sqrt(dxes[0][1] * dxes[1][1])[None, :, None] + + (s[2] - numpy.roll(s[2], 1, axis=2)) / numpy.sqrt(dxes[0][2] * dxes[1][2])[None, None, :] ) return ds + + +def energy_hstep(e0, h1, e2, epsilon=None, mu=None, dxes=None): + u = dxmul(e0 * e2, h1 * h1, epsilon, mu, dxes) + return u + + +def energy_estep(h0, e1, h2, epsilon=None, mu=None, dxes=None): + u = dxmul(e1 * e1, h0 * h2, epsilon, mu, dxes) + return u + + +def delta_energy_h2e(dt, e0, h1, e2, h3, epsilon=None, mu=None, dxes=None): + """ + This is just from (e2 * e2 + h3 * h1) - (h1 * h1 + e0 * e2) + """ + de = e2 * (e2 - e0) / dt + dh = h1 * (h3 - h1) / dt + du = dt * dxmul(de, dh, epsilon, mu, dxes) + return du + + +def delta_energy_e2h(dt, h0, e1, h2, e3, epsilon=None, mu=None, dxes=None): + """ + This is just from (h2 * h2 + e3 * e1) - (e1 * e1 + h0 * h2) + """ + de = e1 * (e3 - e1) / dt + dh = h2 * (h2 - h0) / dt + du = dxmul(de, dh, epsilon, mu, dxes) + return du + + +def delta_energy_j(j0, e1, dxes=None): + if dxes is None: + dxes = tuple(tuple(numpy.ones(1) for _ in range(3)) for _ in range(2)) + + du = ((j0 * e1).sum(axis=0) * + dxes[0][0][:, None, None] * + dxes[0][1][None, :, None] * + dxes[0][2][None, None, :]) + return du + + +def dxmul(ee, hh, epsilon=None, mu=None, dxes=None): + if epsilon is None: + epsilon = 1 + if mu is None: + mu = 1 + if dxes is None: + dxes = tuple(tuple(numpy.ones(1) for _ in range(3)) for _ in range(2)) + + result = ((ee * epsilon).sum(axis=0) * + dxes[0][0][:, None, None] * + dxes[0][1][None, :, None] * + dxes[0][2][None, None, :] + + (hh * mu).sum(axis=0) * + dxes[1][0][:, None, None] * + dxes[1][1][None, :, None] * + dxes[1][2][None, None, :]) + return result + + + diff --git a/fdfd_tools/test_fdtd.py b/fdfd_tools/test_fdtd.py new file mode 100644 index 0000000..ccdc00c --- /dev/null +++ b/fdfd_tools/test_fdtd.py @@ -0,0 +1,68 @@ +import unittest +import numpy + +from fdfd_tools import fdtd + +class TestBasic2D(unittest.TestCase): + def setUp(self): + shape = [3, 5, 5, 1] + dt = 0.5 + epsilon = numpy.ones(shape, dtype=float) + + src_mask = numpy.zeros_like(epsilon, dtype=bool) + src_mask[1, 2, 2, 0] = True + + e = numpy.zeros_like(epsilon) + h = numpy.zeros_like(epsilon) + e[src_mask] = 32 + es = [e] + hs = [h] + + eh2h = fdtd.maxwell_h(dt=dt) + eh2e = fdtd.maxwell_e(dt=dt) + for _ in range(9): + e = e.copy() + h = h.copy() + eh2h(e, h) + eh2e(e, h, epsilon) + es.append(e) + hs.append(h) + + self.es = es + self.hs = hs + self.dt = dt + self.epsilon = epsilon + self.src_mask = src_mask + + def test_initial_fields(self): + # Make sure initial fields didn't change + e0 = self.es[0] + h0 = self.hs[0] + self.assertEqual(e0[1, 2, 2, 0], 32) + + self.assertFalse(e0[~self.src_mask].any()) + self.assertFalse(h0.any()) + + + def test_initial_energy(self): + e0 = self.es[0] + h0 = self.hs[0] + h1 = self.hs[1] + mask = self.src_mask[1] + + # Make sure initial energy and E dot J are correct + energy0 = fdtd.energy_estep(h0=h0, e1=e0, h2=self.hs[1]) + e_dot_j_0 = fdtd.delta_energy_j(j0=e0 - 0, e1=e0) + self.assertEqual(energy0[mask], 32 * 32) + self.assertFalse(energy0[~mask].any()) + self.assertEqual(e_dot_j_0[mask], 32 * 32) + self.assertFalse(e_dot_j_0[~mask].any()) + + + def test_energy_conservation(self): + for ii in range(1, 8): + with self.subTest(i=ii): + u_estep = fdtd.energy_estep(h0=self.hs[ii], e1=self.es[ii], h2=self.hs[ii + 1]) + u_hstep = fdtd.energy_hstep(e0=self.es[ii-1], h1=self.hs[ii], e2=self.es[ii]) + self.assertTrue(numpy.allclose(u_estep.sum(), 32 * 32)) + self.assertTrue(numpy.allclose(u_hstep.sum(), 32 * 32)) From 79e14af4db371d82bfb4c2eec084d06af7ed827b Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Wed, 17 Jul 2019 00:50:49 -0700 Subject: [PATCH 27/77] poynting divergence doesn't use dt, and can have default dxes --- fdfd_tools/fdtd.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/fdfd_tools/fdtd.py b/fdfd_tools/fdtd.py index 0cdec0d..bc52e45 100644 --- a/fdfd_tools/fdtd.py +++ b/fdfd_tools/fdtd.py @@ -263,9 +263,13 @@ def poynting(e, h): return numpy.array(s) -def poynting_divergence(dt, dxes, s=None, *, e=None, h=None): # TODO dxes +def poynting_divergence(s=None, *, e=None, h=None, dxes=None): # TODO dxes + if dxes is None: + dxes = tuple(tuple(numpy.ones(1) for _ in range(3)) for _ in range(2)) + if s is None: s = poynting(e, h) + ds = ((s[0] - numpy.roll(s[0], 1, axis=0)) / numpy.sqrt(dxes[0][0] * dxes[1][0])[:, None, None] + (s[1] - numpy.roll(s[1], 1, axis=1)) / numpy.sqrt(dxes[0][1] * dxes[1][1])[None, :, None] + (s[2] - numpy.roll(s[2], 1, axis=2)) / numpy.sqrt(dxes[0][2] * dxes[1][2])[None, None, :] ) From 935b2c9a80a3f31fa36ee561204d431e76ac524f Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Wed, 17 Jul 2019 00:51:13 -0700 Subject: [PATCH 28/77] remove extra dt --- fdfd_tools/fdtd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fdfd_tools/fdtd.py b/fdfd_tools/fdtd.py index bc52e45..15b5635 100644 --- a/fdfd_tools/fdtd.py +++ b/fdfd_tools/fdtd.py @@ -292,7 +292,7 @@ def delta_energy_h2e(dt, e0, h1, e2, h3, epsilon=None, mu=None, dxes=None): """ de = e2 * (e2 - e0) / dt dh = h1 * (h3 - h1) / dt - du = dt * dxmul(de, dh, epsilon, mu, dxes) + du = dxmul(de, dh, epsilon, mu, dxes) return du From a528effd89263e6b845e9a438a7a41d665648cb9 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Wed, 17 Jul 2019 00:51:28 -0700 Subject: [PATCH 29/77] add some more tests --- fdfd_tools/test_fdtd.py | 177 ++++++++++++++++++++++++++++++++-------- 1 file changed, 145 insertions(+), 32 deletions(-) diff --git a/fdfd_tools/test_fdtd.py b/fdfd_tools/test_fdtd.py index ccdc00c..dc7f852 100644 --- a/fdfd_tools/test_fdtd.py +++ b/fdfd_tools/test_fdtd.py @@ -3,23 +3,94 @@ import numpy from fdfd_tools import fdtd -class TestBasic2D(unittest.TestCase): +class BasicTests(): + def test_initial_fields(self): + # Make sure initial fields didn't change + e0 = self.es[0] + h0 = self.hs[0] + mask = self.src_mask + + self.assertEqual(e0[mask], self.j_mag / self.epsilon[mask]) + self.assertFalse(e0[~mask].any()) + self.assertFalse(h0.any()) + + + def test_initial_energy(self): + e0 = self.es[0] + h0 = self.hs[0] + h1 = self.hs[1] + mask = self.src_mask[1] + u0 = self.j_mag * self.j_mag / self.epsilon[self.src_mask] + args = {'dxes': self.dxes, + 'epsilon': self.epsilon} + + # Make sure initial energy and E dot J are correct + energy0 = fdtd.energy_estep(h0=h0, e1=e0, h2=self.hs[1], **args) + e_dot_j_0 = fdtd.delta_energy_j(j0=(e0 - 0) * self.epsilon, e1=e0, dxes=self.dxes) + self.assertEqual(energy0[mask], u0) + self.assertFalse(energy0[~mask].any()) + self.assertEqual(e_dot_j_0[mask], u0) + self.assertFalse(e_dot_j_0[~mask].any()) + + + def test_energy_conservation(self): + e0 = self.es[0] + u0 = fdtd.delta_energy_j(j0=(e0 - 0) * self.epsilon, e1=e0).sum() + args = {'dxes': self.dxes, + 'epsilon': self.epsilon} + + for ii in range(1, 8): + with self.subTest(i=ii): + u_hstep = fdtd.energy_hstep(e0=self.es[ii-1], h1=self.hs[ii], e2=self.es[ii], **args) + u_estep = fdtd.energy_estep(h0=self.hs[ii], e1=self.es[ii], h2=self.hs[ii + 1], **args) + self.assertTrue(numpy.allclose(u_hstep.sum(), u0)) + self.assertTrue(numpy.allclose(u_estep.sum(), u0)) + + + def test_poynting(self): + args = {'dxes': self.dxes, + 'epsilon': self.epsilon} + + for ii in range(1, 8): + u_eprev = None + with self.subTest(i=ii): + u_hstep = fdtd.energy_hstep(e0=self.es[ii-1], h1=self.hs[ii], e2=self.es[ii], **args) + u_estep = fdtd.energy_estep(h0=self.hs[ii], e1=self.es[ii], h2=self.hs[ii + 1], **args) + + du_half_h2e = u_estep - u_hstep + div_s_h2e = self.dt * fdtd.poynting_divergence(e=self.es[ii], h=self.hs[ii], dxes=self.dxes) + self.assertTrue(numpy.allclose(du_half_h2e, -div_s_h2e)) + + if u_eprev is None: + u_eprev = u_estep + continue + + # previous half-step + du_half_e2h = u_hstep - u_eprev + div_s_e2h = self.dt * fdtd.poynting_divergence(e=self.es[ii], h=self.hs[ii-1], dxes=self.dxes) + self.assertTrue(numpy.allclose(du_half_e2h, -div_s_e2h)) + u_eprev = u_estep + + +class Basic2DNoDXOnlyVacuum(unittest.TestCase, BasicTests): def setUp(self): shape = [3, 5, 5, 1] dt = 0.5 epsilon = numpy.ones(shape, dtype=float) + j_mag = 32 + dxes = None src_mask = numpy.zeros_like(epsilon, dtype=bool) src_mask[1, 2, 2, 0] = True e = numpy.zeros_like(epsilon) h = numpy.zeros_like(epsilon) - e[src_mask] = 32 + e[src_mask] = j_mag / epsilon[src_mask] es = [e] hs = [h] - eh2h = fdtd.maxwell_h(dt=dt) - eh2e = fdtd.maxwell_e(dt=dt) + eh2h = fdtd.maxwell_h(dt=dt, dxes=dxes) + eh2e = fdtd.maxwell_e(dt=dt, dxes=dxes) for _ in range(9): e = e.copy() h = h.copy() @@ -32,37 +103,79 @@ class TestBasic2D(unittest.TestCase): self.hs = hs self.dt = dt self.epsilon = epsilon + self.dxes = dxes self.src_mask = src_mask - - def test_initial_fields(self): - # Make sure initial fields didn't change - e0 = self.es[0] - h0 = self.hs[0] - self.assertEqual(e0[1, 2, 2, 0], 32) - - self.assertFalse(e0[~self.src_mask].any()) - self.assertFalse(h0.any()) + self.j_mag = j_mag - def test_initial_energy(self): - e0 = self.es[0] - h0 = self.hs[0] - h1 = self.hs[1] - mask = self.src_mask[1] +class Basic3DUniformDXOnlyVacuum(unittest.TestCase, BasicTests): + def setUp(self): + shape = [3, 5, 5, 5] + dt = 0.33 + epsilon = numpy.ones(shape, dtype=float) + j_mag = 32 + dxes = tuple(tuple(numpy.ones(s) for s in shape[1:]) for _ in range(2)) - # Make sure initial energy and E dot J are correct - energy0 = fdtd.energy_estep(h0=h0, e1=e0, h2=self.hs[1]) - e_dot_j_0 = fdtd.delta_energy_j(j0=e0 - 0, e1=e0) - self.assertEqual(energy0[mask], 32 * 32) - self.assertFalse(energy0[~mask].any()) - self.assertEqual(e_dot_j_0[mask], 32 * 32) - self.assertFalse(e_dot_j_0[~mask].any()) + src_mask = numpy.zeros_like(epsilon, dtype=bool) + src_mask[1, 2, 2, 0] = True + + e = numpy.zeros_like(epsilon) + h = numpy.zeros_like(epsilon) + e[src_mask] = j_mag / epsilon[src_mask] + es = [e] + hs = [h] + + eh2h = fdtd.maxwell_h(dt=dt, dxes=dxes) + eh2e = fdtd.maxwell_e(dt=dt, dxes=dxes) + for _ in range(9): + e = e.copy() + h = h.copy() + eh2h(e, h) + eh2e(e, h, epsilon) + es.append(e) + hs.append(h) + + self.es = es + self.hs = hs + self.dt = dt + self.epsilon = epsilon + self.dxes = dxes + self.src_mask = src_mask + self.j_mag = j_mag - def test_energy_conservation(self): - for ii in range(1, 8): - with self.subTest(i=ii): - u_estep = fdtd.energy_estep(h0=self.hs[ii], e1=self.es[ii], h2=self.hs[ii + 1]) - u_hstep = fdtd.energy_hstep(e0=self.es[ii-1], h1=self.hs[ii], e2=self.es[ii]) - self.assertTrue(numpy.allclose(u_estep.sum(), 32 * 32)) - self.assertTrue(numpy.allclose(u_hstep.sum(), 32 * 32)) +class Basic3DUniformDX(unittest.TestCase, BasicTests): + def setUp(self): + shape = [3, 5, 5, 5] + dt = 0.33 + epsilon = numpy.full(shape, 2, dtype=float) + j_mag = 32 + dxes = tuple(tuple(numpy.ones(s) for s in shape[1:]) for _ in range(2)) + + src_mask = numpy.zeros_like(epsilon, dtype=bool) + src_mask[1, 2, 2, 0] = True + + e = numpy.zeros_like(epsilon) + h = numpy.zeros_like(epsilon) + e[src_mask] = j_mag / epsilon[src_mask] + es = [e] + hs = [h] + + eh2h = fdtd.maxwell_h(dt=dt, dxes=dxes) + eh2e = fdtd.maxwell_e(dt=dt, dxes=dxes) + for _ in range(9): + e = e.copy() + h = h.copy() + eh2h(e, h) + eh2e(e, h, epsilon) + es.append(e) + hs.append(h) + + self.es = es + self.hs = hs + self.dt = dt + self.epsilon = epsilon + self.dxes = dxes + self.src_mask = src_mask + self.j_mag = j_mag + From 2cec4fabaf83cc83e13a8ec2155d8c2927560e58 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Wed, 17 Jul 2019 23:47:45 -0700 Subject: [PATCH 30/77] Account for dxes --- fdfd_tools/test_fdtd.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/fdfd_tools/test_fdtd.py b/fdfd_tools/test_fdtd.py index dc7f852..33f51b0 100644 --- a/fdfd_tools/test_fdtd.py +++ b/fdfd_tools/test_fdtd.py @@ -20,7 +20,9 @@ class BasicTests(): h0 = self.hs[0] h1 = self.hs[1] mask = self.src_mask[1] - u0 = self.j_mag * self.j_mag / self.epsilon[self.src_mask] + dxes = self.dxes if self.dxes is not None else tuple(tuple(numpy.ones(s) for s in e0.shape[1:]) for _ in range(2)) + dV = numpy.prod(numpy.meshgrid(*dxes[0], indexing='ij'), axis=0) + u0 = self.j_mag * self.j_mag / self.epsilon[self.src_mask] * dV[mask] args = {'dxes': self.dxes, 'epsilon': self.epsilon} @@ -35,7 +37,7 @@ class BasicTests(): def test_energy_conservation(self): e0 = self.es[0] - u0 = fdtd.delta_energy_j(j0=(e0 - 0) * self.epsilon, e1=e0).sum() + u0 = fdtd.delta_energy_j(j0=(e0 - 0) * self.epsilon, e1=e0, dxes=self.dxes).sum() args = {'dxes': self.dxes, 'epsilon': self.epsilon} From f858cb8bbb0d38bf62a47b7aeb6a7d3fd5a59418 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Wed, 17 Jul 2019 23:48:04 -0700 Subject: [PATCH 31/77] Fix poynting e2h test --- fdfd_tools/test_fdtd.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fdfd_tools/test_fdtd.py b/fdfd_tools/test_fdtd.py index 33f51b0..7e1e914 100644 --- a/fdfd_tools/test_fdtd.py +++ b/fdfd_tools/test_fdtd.py @@ -53,8 +53,8 @@ class BasicTests(): args = {'dxes': self.dxes, 'epsilon': self.epsilon} + u_eprev = None for ii in range(1, 8): - u_eprev = None with self.subTest(i=ii): u_hstep = fdtd.energy_hstep(e0=self.es[ii-1], h1=self.hs[ii], e2=self.es[ii], **args) u_estep = fdtd.energy_estep(h0=self.hs[ii], e1=self.es[ii], h2=self.hs[ii + 1], **args) @@ -69,7 +69,7 @@ class BasicTests(): # previous half-step du_half_e2h = u_hstep - u_eprev - div_s_e2h = self.dt * fdtd.poynting_divergence(e=self.es[ii], h=self.hs[ii-1], dxes=self.dxes) + div_s_e2h = self.dt * fdtd.poynting_divergence(e=self.es[ii-1], h=self.hs[ii], dxes=self.dxes) self.assertTrue(numpy.allclose(du_half_e2h, -div_s_e2h)) u_eprev = u_estep From 950e70213a9b78d8f8d2ba4073749e879be8773c Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Thu, 18 Jul 2019 00:03:32 -0700 Subject: [PATCH 32/77] Consolidate variables in test case setups --- fdfd_tools/test_fdtd.py | 119 ++++++++++++++++------------------------ 1 file changed, 48 insertions(+), 71 deletions(-) diff --git a/fdfd_tools/test_fdtd.py b/fdfd_tools/test_fdtd.py index 7e1e914..dd0192e 100644 --- a/fdfd_tools/test_fdtd.py +++ b/fdfd_tools/test_fdtd.py @@ -77,107 +77,84 @@ class BasicTests(): class Basic2DNoDXOnlyVacuum(unittest.TestCase, BasicTests): def setUp(self): shape = [3, 5, 5, 1] - dt = 0.5 - epsilon = numpy.ones(shape, dtype=float) - j_mag = 32 - dxes = None + self.dt = 0.5 + self.epsilon = numpy.ones(shape, dtype=float) + self.j_mag = 32 + self.dxes = None - src_mask = numpy.zeros_like(epsilon, dtype=bool) - src_mask[1, 2, 2, 0] = True + self.src_mask = numpy.zeros_like(self.epsilon, dtype=bool) + self.src_mask[1, 2, 2, 0] = True - e = numpy.zeros_like(epsilon) - h = numpy.zeros_like(epsilon) - e[src_mask] = j_mag / epsilon[src_mask] - es = [e] - hs = [h] + e = numpy.zeros_like(self.epsilon) + h = numpy.zeros_like(self.epsilon) + e[self.src_mask] = self.j_mag / self.epsilon[self.src_mask] + self.es = [e] + self.hs = [h] - eh2h = fdtd.maxwell_h(dt=dt, dxes=dxes) - eh2e = fdtd.maxwell_e(dt=dt, dxes=dxes) + eh2h = fdtd.maxwell_h(dt=self.dt, dxes=self.dxes) + eh2e = fdtd.maxwell_e(dt=self.dt, dxes=self.dxes) for _ in range(9): e = e.copy() h = h.copy() eh2h(e, h) - eh2e(e, h, epsilon) - es.append(e) - hs.append(h) + eh2e(e, h, self.epsilon) + self.es.append(e) + self.hs.append(h) - self.es = es - self.hs = hs - self.dt = dt - self.epsilon = epsilon - self.dxes = dxes - self.src_mask = src_mask - self.j_mag = j_mag class Basic3DUniformDXOnlyVacuum(unittest.TestCase, BasicTests): def setUp(self): shape = [3, 5, 5, 5] - dt = 0.33 - epsilon = numpy.ones(shape, dtype=float) - j_mag = 32 - dxes = tuple(tuple(numpy.ones(s) for s in shape[1:]) for _ in range(2)) + self.dt = 0.33 + self.epsilon = numpy.ones(shape, dtype=float) + self.j_mag = 32 + self.dxes = tuple(tuple(numpy.ones(s) for s in shape[1:]) for _ in range(2)) - src_mask = numpy.zeros_like(epsilon, dtype=bool) - src_mask[1, 2, 2, 0] = True + self.src_mask = numpy.zeros_like(self.epsilon, dtype=bool) + self.src_mask[1, 2, 2, 0] = True - e = numpy.zeros_like(epsilon) - h = numpy.zeros_like(epsilon) - e[src_mask] = j_mag / epsilon[src_mask] - es = [e] - hs = [h] + e = numpy.zeros_like(self.epsilon) + h = numpy.zeros_like(self.epsilon) + e[self.src_mask] = self.j_mag / self.epsilon[self.src_mask] + self.es = [e] + self.hs = [h] - eh2h = fdtd.maxwell_h(dt=dt, dxes=dxes) - eh2e = fdtd.maxwell_e(dt=dt, dxes=dxes) + eh2h = fdtd.maxwell_h(dt=self.dt, dxes=self.dxes) + eh2e = fdtd.maxwell_e(dt=self.dt, dxes=self.dxes) for _ in range(9): e = e.copy() h = h.copy() eh2h(e, h) - eh2e(e, h, epsilon) - es.append(e) - hs.append(h) + eh2e(e, h, self.epsilon) + self.es.append(e) + self.hs.append(h) - self.es = es - self.hs = hs - self.dt = dt - self.epsilon = epsilon - self.dxes = dxes - self.src_mask = src_mask - self.j_mag = j_mag class Basic3DUniformDX(unittest.TestCase, BasicTests): def setUp(self): shape = [3, 5, 5, 5] - dt = 0.33 - epsilon = numpy.full(shape, 2, dtype=float) - j_mag = 32 - dxes = tuple(tuple(numpy.ones(s) for s in shape[1:]) for _ in range(2)) + self.dt = 0.33 + self.epsilon = numpy.full(shape, 2, dtype=float) + self.j_mag = 32 + self.dxes = tuple(tuple(numpy.ones(s) for s in shape[1:]) for _ in range(2)) - src_mask = numpy.zeros_like(epsilon, dtype=bool) - src_mask[1, 2, 2, 0] = True + self.src_mask = numpy.zeros_like(self.epsilon, dtype=bool) + self.src_mask[1, 2, 2, 0] = True - e = numpy.zeros_like(epsilon) - h = numpy.zeros_like(epsilon) - e[src_mask] = j_mag / epsilon[src_mask] - es = [e] - hs = [h] + e = numpy.zeros_like(self.epsilon) + h = numpy.zeros_like(self.epsilon) + e[self.src_mask] = self.j_mag / self.epsilon[self.src_mask] + self.es = [e] + self.hs = [h] - eh2h = fdtd.maxwell_h(dt=dt, dxes=dxes) - eh2e = fdtd.maxwell_e(dt=dt, dxes=dxes) + eh2h = fdtd.maxwell_h(dt=self.dt, dxes=self.dxes) + eh2e = fdtd.maxwell_e(dt=self.dt, dxes=self.dxes) for _ in range(9): e = e.copy() h = h.copy() eh2h(e, h) - eh2e(e, h, epsilon) - es.append(e) - hs.append(h) - - self.es = es - self.hs = hs - self.dt = dt - self.epsilon = epsilon - self.dxes = dxes - self.src_mask = src_mask - self.j_mag = j_mag - + eh2e(e, h, self.epsilon) + self.es.append(e) + self.hs.append(h) From fb3c88a78dcf6a6bdcbb47294eb2b49f0bb10c58 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Fri, 19 Jul 2019 00:19:32 -0700 Subject: [PATCH 33/77] add test_poynting_planes --- fdfd_tools/test_fdtd.py | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/fdfd_tools/test_fdtd.py b/fdfd_tools/test_fdtd.py index dd0192e..692cd61 100644 --- a/fdfd_tools/test_fdtd.py +++ b/fdfd_tools/test_fdtd.py @@ -3,6 +3,7 @@ import numpy from fdfd_tools import fdtd + class BasicTests(): def test_initial_fields(self): # Make sure initial fields didn't change @@ -49,7 +50,7 @@ class BasicTests(): self.assertTrue(numpy.allclose(u_estep.sum(), u0)) - def test_poynting(self): + def test_poynting_divergence(self): args = {'dxes': self.dxes, 'epsilon': self.epsilon} @@ -74,6 +75,29 @@ class BasicTests(): u_eprev = u_estep + def test_poynting_planes(self): + args = {'dxes': self.dxes, + 'epsilon': self.epsilon} + + u_eprev = None + for ii in range(1, 8): + with self.subTest(i=ii): + u_hstep = fdtd.energy_hstep(e0=self.es[ii-1], h1=self.hs[ii], e2=self.es[ii], **args) + u_estep = fdtd.energy_estep(h0=self.hs[ii], e1=self.es[ii], h2=self.hs[ii + 1], **args) + + mx = numpy.roll(self.src_mask, (-1, -1), axis=(0, 1)) + my = numpy.roll(self.src_mask, -1, axis=2) + mz = numpy.roll(self.src_mask, (+1, -1), axis=(0, 3)) + px = numpy.roll(self.src_mask, -1, axis=0) + py = self.src_mask.copy() + pz = numpy.roll(self.src_mask, +1, axis=0) + s_h2e = -fdtd.poynting(e=self.es[ii], h=self.hs[ii]) * self.dt + planes = [s_h2e[px].sum(), -s_h2e[mx].sum(), + s_h2e[py].sum(), -s_h2e[my].sum(), + s_h2e[pz].sum(), -s_h2e[mz].sum()] + self.assertTrue(numpy.allclose(sum(planes), (u_estep - u_hstep)[self.src_mask[1]])) + + class Basic2DNoDXOnlyVacuum(unittest.TestCase, BasicTests): def setUp(self): shape = [3, 5, 5, 1] From 223b202d03c8a5298732886bc8b368eca0487029 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Fri, 19 Jul 2019 00:19:47 -0700 Subject: [PATCH 34/77] More test cases --- fdfd_tools/test_fdtd.py | 41 +++++++++++++++++++++++++++++++++++------ 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/fdfd_tools/test_fdtd.py b/fdfd_tools/test_fdtd.py index 692cd61..bc995c1 100644 --- a/fdfd_tools/test_fdtd.py +++ b/fdfd_tools/test_fdtd.py @@ -126,17 +126,16 @@ class Basic2DNoDXOnlyVacuum(unittest.TestCase, BasicTests): self.hs.append(h) - class Basic3DUniformDXOnlyVacuum(unittest.TestCase, BasicTests): def setUp(self): shape = [3, 5, 5, 5] - self.dt = 0.33 + self.dt = 0.5 self.epsilon = numpy.ones(shape, dtype=float) self.j_mag = 32 self.dxes = tuple(tuple(numpy.ones(s) for s in shape[1:]) for _ in range(2)) self.src_mask = numpy.zeros_like(self.epsilon, dtype=bool) - self.src_mask[1, 2, 2, 0] = True + self.src_mask[1, 2, 2, 2] = True e = numpy.zeros_like(self.epsilon) h = numpy.zeros_like(self.epsilon) @@ -156,16 +155,46 @@ class Basic3DUniformDXOnlyVacuum(unittest.TestCase, BasicTests): +class Basic3DUniformDXUniformN(unittest.TestCase, BasicTests): + def setUp(self): + shape = [3, 5, 5, 5] + self.dt = 0.5 + self.epsilon = numpy.full(shape, 2, dtype=float) + self.j_mag = 32 + self.dxes = tuple(tuple(numpy.ones(s) for s in shape[1:]) for _ in range(2)) + + self.src_mask = numpy.zeros_like(self.epsilon, dtype=bool) + self.src_mask[1, 2, 2, 2] = True + + e = numpy.zeros_like(self.epsilon) + h = numpy.zeros_like(self.epsilon) + e[self.src_mask] = self.j_mag / self.epsilon[self.src_mask] + self.es = [e] + self.hs = [h] + + eh2h = fdtd.maxwell_h(dt=self.dt, dxes=self.dxes) + eh2e = fdtd.maxwell_e(dt=self.dt, dxes=self.dxes) + for _ in range(9): + e = e.copy() + h = h.copy() + eh2h(e, h) + eh2e(e, h, self.epsilon) + self.es.append(e) + self.hs.append(h) + + class Basic3DUniformDX(unittest.TestCase, BasicTests): def setUp(self): shape = [3, 5, 5, 5] self.dt = 0.33 - self.epsilon = numpy.full(shape, 2, dtype=float) self.j_mag = 32 self.dxes = tuple(tuple(numpy.ones(s) for s in shape[1:]) for _ in range(2)) - self.src_mask = numpy.zeros_like(self.epsilon, dtype=bool) - self.src_mask[1, 2, 2, 0] = True + self.src_mask = numpy.zeros(shape, dtype=bool) + self.src_mask[1, 2, 2, 2] = True + + self.epsilon = numpy.full(shape, 1, dtype=float) + self.epsilon[self.src_mask] = 2 e = numpy.zeros_like(self.epsilon) h = numpy.zeros_like(self.epsilon) From 30ddeb7b73e49e88acedd0dcb7d179dbcc1f0007 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 21 Jul 2019 22:05:40 -0700 Subject: [PATCH 35/77] fix typo in fdfd.vec() --- fdfd_tools/vectorization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fdfd_tools/vectorization.py b/fdfd_tools/vectorization.py index bdc9d6a..57b58fb 100644 --- a/fdfd_tools/vectorization.py +++ b/fdfd_tools/vectorization.py @@ -45,5 +45,5 @@ def unvec(v: vfield_t, shape: numpy.ndarray) -> field_t: """ if numpy.any(numpy.equal(v, None)): return None - return vi.reshape((3, *shape), order='C') + return v.reshape((3, *shape), order='C') From 7f8a3261149e4fb2ca7adb3c9837b74e22ab5311 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 21 Jul 2019 22:06:24 -0700 Subject: [PATCH 36/77] Loosen tolerances on tests --- fdfd_tools/test_fdtd.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fdfd_tools/test_fdtd.py b/fdfd_tools/test_fdtd.py index bc995c1..a88ca75 100644 --- a/fdfd_tools/test_fdtd.py +++ b/fdfd_tools/test_fdtd.py @@ -62,7 +62,7 @@ class BasicTests(): du_half_h2e = u_estep - u_hstep div_s_h2e = self.dt * fdtd.poynting_divergence(e=self.es[ii], h=self.hs[ii], dxes=self.dxes) - self.assertTrue(numpy.allclose(du_half_h2e, -div_s_h2e)) + self.assertTrue(numpy.allclose(du_half_h2e, -div_s_h2e, rtol=1e-4)) if u_eprev is None: u_eprev = u_estep @@ -71,7 +71,7 @@ class BasicTests(): # previous half-step du_half_e2h = u_hstep - u_eprev div_s_e2h = self.dt * fdtd.poynting_divergence(e=self.es[ii-1], h=self.hs[ii], dxes=self.dxes) - self.assertTrue(numpy.allclose(du_half_e2h, -div_s_e2h)) + self.assertTrue(numpy.allclose(du_half_e2h, -div_s_e2h, rtol=1e-4)) u_eprev = u_estep From f1fc308d25b24954513e57bf65c07a909b9f27a9 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 21 Jul 2019 22:06:57 -0700 Subject: [PATCH 37/77] Add JdotE test --- fdfd_tools/test_fdtd.py | 50 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/fdfd_tools/test_fdtd.py b/fdfd_tools/test_fdtd.py index a88ca75..0b6278f 100644 --- a/fdfd_tools/test_fdtd.py +++ b/fdfd_tools/test_fdtd.py @@ -96,6 +96,7 @@ class BasicTests(): s_h2e[py].sum(), -s_h2e[my].sum(), s_h2e[pz].sum(), -s_h2e[mz].sum()] self.assertTrue(numpy.allclose(sum(planes), (u_estep - u_hstep)[self.src_mask[1]])) +# print(planes, '\n', numpy.rollaxis(u_estep - u_hstep, -1), sum(planes)) class Basic2DNoDXOnlyVacuum(unittest.TestCase, BasicTests): @@ -211,3 +212,52 @@ class Basic3DUniformDX(unittest.TestCase, BasicTests): eh2e(e, h, self.epsilon) self.es.append(e) self.hs.append(h) + + +class JdotE_3DUniformDX(unittest.TestCase): + def setUp(self): + shape = [3, 5, 5, 5] + self.dt = 0.5 + self.j_mag = 32 + self.dxes = tuple(tuple(numpy.full(s, 2.0) for s in shape[1:]) for _ in range(2)) + + self.src_mask = numpy.zeros(shape, dtype=bool) + self.src_mask[1, 2, 2, 2] = True + + self.epsilon = numpy.full(shape, 4, dtype=float) + self.epsilon[self.src_mask] = 2 + + e = numpy.random.randint(-128, 128 + 1, size=shape).astype(numpy.float32) + h = numpy.random.randint(-128, 128 + 1, size=shape).astype(numpy.float32) + self.es = [e] + self.hs = [h] + + eh2h = fdtd.maxwell_h(dt=self.dt, dxes=self.dxes) + eh2e = fdtd.maxwell_e(dt=self.dt, dxes=self.dxes) + for ii in range(9): + e = e.copy() + h = h.copy() + eh2h(e, h) + eh2e(e, h, self.epsilon) + self.es.append(e) + self.hs.append(h) + + if ii == 1: + e[self.src_mask] += self.j_mag / self.epsilon[self.src_mask] + self.j_dot_e = self.j_mag * e[self.src_mask] + + + def test_j_dot_e(self): + e0 = self.es[2] + j0 = numpy.zeros_like(e0) + j0[self.src_mask] = self.j_mag + u0 = fdtd.delta_energy_j(j0=j0, e1=e0, dxes=self.dxes) + args = {'dxes': self.dxes, + 'epsilon': self.epsilon} + + ii=2 + u_hstep = fdtd.energy_hstep(e0=self.es[ii-1], h1=self.hs[ii], e2=self.es[ii], **args) + u_estep = fdtd.energy_estep(h0=self.hs[ii], e1=self.es[ii], h2=self.hs[ii + 1], **args) + #print(u0.sum(), (u_estep - u_hstep).sum()) + self.assertTrue(numpy.allclose(u0.sum(), (u_estep - u_hstep).sum(), rtol=1e-4)) + From 7092c13088f27413b1d4573bca7cce158c01f7bc Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 22 Jul 2019 00:26:34 -0700 Subject: [PATCH 38/77] better error messages when tests fail --- fdfd_tools/test_fdtd.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/fdfd_tools/test_fdtd.py b/fdfd_tools/test_fdtd.py index 0b6278f..247bd9c 100644 --- a/fdfd_tools/test_fdtd.py +++ b/fdfd_tools/test_fdtd.py @@ -31,9 +31,9 @@ class BasicTests(): energy0 = fdtd.energy_estep(h0=h0, e1=e0, h2=self.hs[1], **args) e_dot_j_0 = fdtd.delta_energy_j(j0=(e0 - 0) * self.epsilon, e1=e0, dxes=self.dxes) self.assertEqual(energy0[mask], u0) - self.assertFalse(energy0[~mask].any()) + self.assertFalse(energy0[~mask].any(), msg='energy0: {}'.format(energy0)) self.assertEqual(e_dot_j_0[mask], u0) - self.assertFalse(e_dot_j_0[~mask].any()) + self.assertFalse(e_dot_j_0[~mask].any(), msg='e_dot_j_0: {}'.format(e_dot_j_0)) def test_energy_conservation(self): @@ -46,8 +46,8 @@ class BasicTests(): with self.subTest(i=ii): u_hstep = fdtd.energy_hstep(e0=self.es[ii-1], h1=self.hs[ii], e2=self.es[ii], **args) u_estep = fdtd.energy_estep(h0=self.hs[ii], e1=self.es[ii], h2=self.hs[ii + 1], **args) - self.assertTrue(numpy.allclose(u_hstep.sum(), u0)) - self.assertTrue(numpy.allclose(u_estep.sum(), u0)) + self.assertTrue(numpy.allclose(u_hstep.sum(), u0), msg='u_hstep: {}\n{}'.format(u_hstep.sum(), numpy.rollaxis(u_hstep, -1))) + self.assertTrue(numpy.allclose(u_estep.sum(), u0), msg='u_estep: {}\n{}'.format(u_estep.sum(), numpy.rollaxis(u_estep, -1))) def test_poynting_divergence(self): @@ -63,6 +63,9 @@ class BasicTests(): du_half_h2e = u_estep - u_hstep div_s_h2e = self.dt * fdtd.poynting_divergence(e=self.es[ii], h=self.hs[ii], dxes=self.dxes) self.assertTrue(numpy.allclose(du_half_h2e, -div_s_h2e, rtol=1e-4)) + self.assertTrue(numpy.allclose(du_half_h2e, -div_s_h2e, rtol=1e-4), + msg='du_half_h2e\n{}\ndiv_s_h2e\n{}'.format(numpy.rollaxis(du_half_h2e, -1), + -numpy.rollaxis(div_s_h2e, -1))) if u_eprev is None: u_eprev = u_estep @@ -72,6 +75,9 @@ class BasicTests(): du_half_e2h = u_hstep - u_eprev div_s_e2h = self.dt * fdtd.poynting_divergence(e=self.es[ii-1], h=self.hs[ii], dxes=self.dxes) self.assertTrue(numpy.allclose(du_half_e2h, -div_s_e2h, rtol=1e-4)) + self.assertTrue(numpy.allclose(du_half_e2h, -div_s_e2h, rtol=1e-4), + msg='du_half_e2h\n{}\ndiv_s_e2h\n{}'.format(numpy.rollaxis(du_half_e2h, -1), + -numpy.rollaxis(div_s_e2h, -1))) u_eprev = u_estep @@ -95,8 +101,8 @@ class BasicTests(): planes = [s_h2e[px].sum(), -s_h2e[mx].sum(), s_h2e[py].sum(), -s_h2e[my].sum(), s_h2e[pz].sum(), -s_h2e[mz].sum()] - self.assertTrue(numpy.allclose(sum(planes), (u_estep - u_hstep)[self.src_mask[1]])) -# print(planes, '\n', numpy.rollaxis(u_estep - u_hstep, -1), sum(planes)) + self.assertTrue(numpy.allclose(sum(planes), (u_estep - u_hstep)[self.src_mask[1]]), + msg='planes: {} (sum: {})\n du:\n {}'.format(planes, sum(planes), (u_estep - u_hstep)[self.src_mask[1]])) class Basic2DNoDXOnlyVacuum(unittest.TestCase, BasicTests): From 89976647f2a5fdbcbcc62c03c4ef3ae2c7594269 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 22 Jul 2019 00:27:32 -0700 Subject: [PATCH 39/77] test (and fix tests) for constant non-1 dxes Still need to look at non-constant dxes --- fdfd_tools/test_fdtd.py | 78 ++++++++++++++++++++++++++++++++++++++--- 1 file changed, 74 insertions(+), 4 deletions(-) diff --git a/fdfd_tools/test_fdtd.py b/fdfd_tools/test_fdtd.py index 247bd9c..fb29cfb 100644 --- a/fdfd_tools/test_fdtd.py +++ b/fdfd_tools/test_fdtd.py @@ -54,6 +54,9 @@ class BasicTests(): args = {'dxes': self.dxes, 'epsilon': self.epsilon} + dxes = self.dxes if self.dxes is not None else tuple(tuple(numpy.ones(s) for s in self.epsilon.shape[1:]) for _ in range(2)) + dV = numpy.prod(numpy.meshgrid(*dxes[0], indexing='ij'), axis=0) + u_eprev = None for ii in range(1, 8): with self.subTest(i=ii): @@ -61,8 +64,7 @@ class BasicTests(): u_estep = fdtd.energy_estep(h0=self.hs[ii], e1=self.es[ii], h2=self.hs[ii + 1], **args) du_half_h2e = u_estep - u_hstep - div_s_h2e = self.dt * fdtd.poynting_divergence(e=self.es[ii], h=self.hs[ii], dxes=self.dxes) - self.assertTrue(numpy.allclose(du_half_h2e, -div_s_h2e, rtol=1e-4)) + div_s_h2e = self.dt * fdtd.poynting_divergence(e=self.es[ii], h=self.hs[ii], dxes=self.dxes) * dV self.assertTrue(numpy.allclose(du_half_h2e, -div_s_h2e, rtol=1e-4), msg='du_half_h2e\n{}\ndiv_s_h2e\n{}'.format(numpy.rollaxis(du_half_h2e, -1), -numpy.rollaxis(div_s_h2e, -1))) @@ -73,8 +75,7 @@ class BasicTests(): # previous half-step du_half_e2h = u_hstep - u_eprev - div_s_e2h = self.dt * fdtd.poynting_divergence(e=self.es[ii-1], h=self.hs[ii], dxes=self.dxes) - self.assertTrue(numpy.allclose(du_half_e2h, -div_s_e2h, rtol=1e-4)) + div_s_e2h = self.dt * fdtd.poynting_divergence(e=self.es[ii-1], h=self.hs[ii], dxes=self.dxes) * dV self.assertTrue(numpy.allclose(du_half_e2h, -div_s_e2h, rtol=1e-4), msg='du_half_e2h\n{}\ndiv_s_e2h\n{}'.format(numpy.rollaxis(du_half_e2h, -1), -numpy.rollaxis(div_s_e2h, -1))) @@ -84,6 +85,8 @@ class BasicTests(): def test_poynting_planes(self): args = {'dxes': self.dxes, 'epsilon': self.epsilon} + dxes = self.dxes if self.dxes is not None else tuple(tuple(numpy.ones(s) for s in self.epsilon.shape[1:]) for _ in range(2)) + dV = numpy.prod(numpy.meshgrid(*dxes[0], indexing='ij'), axis=0) u_eprev = None for ii in range(1, 8): @@ -98,6 +101,9 @@ class BasicTests(): py = self.src_mask.copy() pz = numpy.roll(self.src_mask, +1, axis=0) s_h2e = -fdtd.poynting(e=self.es[ii], h=self.hs[ii]) * self.dt + s_h2e[0] *= dxes[0][1][None, :, None] * dxes[0][2][None, None, :] + s_h2e[1] *= dxes[0][0][:, None, None] * dxes[0][2][None, None, :] + s_h2e[2] *= dxes[0][0][:, None, None] * dxes[0][1][None, :, None] planes = [s_h2e[px].sum(), -s_h2e[mx].sum(), s_h2e[py].sum(), -s_h2e[my].sum(), s_h2e[pz].sum(), -s_h2e[mz].sum()] @@ -133,6 +139,36 @@ class Basic2DNoDXOnlyVacuum(unittest.TestCase, BasicTests): self.hs.append(h) +class Basic2DUniformDX3(unittest.TestCase, BasicTests): + def setUp(self): + shape = [3, 5, 5, 1] + self.dt = 0.5 + self.j_mag = 32 + self.dxes = tuple(tuple(numpy.full(s, 2.0) for s in shape[1:]) for _ in range(2)) + + self.src_mask = numpy.zeros(shape, dtype=bool) + self.src_mask[1, 2, 2, 0] = True + + self.epsilon = numpy.full(shape, 1, dtype=float) + self.epsilon[self.src_mask] = 2 + + e = numpy.zeros_like(self.epsilon) + h = numpy.zeros_like(self.epsilon) + e[self.src_mask] = self.j_mag / self.epsilon[self.src_mask] + self.es = [e] + self.hs = [h] + + eh2h = fdtd.maxwell_h(dt=self.dt, dxes=self.dxes) + eh2e = fdtd.maxwell_e(dt=self.dt, dxes=self.dxes) + for _ in range(9): + e = e.copy() + h = h.copy() + eh2h(e, h) + eh2e(e, h, self.epsilon) + self.es.append(e) + self.hs.append(h) + + class Basic3DUniformDXOnlyVacuum(unittest.TestCase, BasicTests): def setUp(self): shape = [3, 5, 5, 5] @@ -220,6 +256,40 @@ class Basic3DUniformDX(unittest.TestCase, BasicTests): self.hs.append(h) +class Basic3DUniformDX3(unittest.TestCase, BasicTests): + def setUp(self): + shape = [3, 5, 5, 5] + self.dt = 0.5 + self.j_mag = 32 + self.dxes = tuple(tuple(numpy.full(s, 3.0) for s in shape[1:]) for _ in range(2)) + + self.src_mask = numpy.zeros(shape, dtype=bool) + self.src_mask[1, 2, 2, 2] = True + + self.epsilon = numpy.full(shape, 1, dtype=float) + self.epsilon[self.src_mask] = 2 + + e = numpy.zeros_like(self.epsilon) + h = numpy.zeros_like(self.epsilon) + e[self.src_mask] = self.j_mag / self.epsilon[self.src_mask] + self.es = [e] + self.hs = [h] + + eh2h = fdtd.maxwell_h(dt=self.dt, dxes=self.dxes) + eh2e = fdtd.maxwell_e(dt=self.dt, dxes=self.dxes) + for _ in range(9): + e = e.copy() + h = h.copy() + eh2h(e, h) + eh2e(e, h, self.epsilon) + self.es.append(e) + self.hs.append(h) + logging.basicConfig(level=logging.DEBUG) + + def tearDown(self): + logging.basicConfig(level=logging.INFO) + + class JdotE_3DUniformDX(unittest.TestCase): def setUp(self): shape = [3, 5, 5, 5] From 39c05d2cabbbf15255c6f575e985c6568720ce51 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 22 Jul 2019 00:27:48 -0700 Subject: [PATCH 40/77] no reason to demand float32 yet --- fdfd_tools/test_fdtd.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fdfd_tools/test_fdtd.py b/fdfd_tools/test_fdtd.py index fb29cfb..6628ab6 100644 --- a/fdfd_tools/test_fdtd.py +++ b/fdfd_tools/test_fdtd.py @@ -303,8 +303,8 @@ class JdotE_3DUniformDX(unittest.TestCase): self.epsilon = numpy.full(shape, 4, dtype=float) self.epsilon[self.src_mask] = 2 - e = numpy.random.randint(-128, 128 + 1, size=shape).astype(numpy.float32) - h = numpy.random.randint(-128, 128 + 1, size=shape).astype(numpy.float32) + e = numpy.random.randint(-128, 128 + 1, size=shape).astype(float) + h = numpy.random.randint(-128, 128 + 1, size=shape).astype(float) self.es = [e] self.hs = [h] From b4bbfdb7300f44cbe01660a8f941609cabd3f884 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Wed, 24 Jul 2019 22:42:11 -0700 Subject: [PATCH 41/77] remove old logging stuff --- fdfd_tools/test_fdtd.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/fdfd_tools/test_fdtd.py b/fdfd_tools/test_fdtd.py index 6628ab6..879d3d4 100644 --- a/fdfd_tools/test_fdtd.py +++ b/fdfd_tools/test_fdtd.py @@ -284,10 +284,6 @@ class Basic3DUniformDX3(unittest.TestCase, BasicTests): eh2e(e, h, self.epsilon) self.es.append(e) self.hs.append(h) - logging.basicConfig(level=logging.DEBUG) - - def tearDown(self): - logging.basicConfig(level=logging.INFO) class JdotE_3DUniformDX(unittest.TestCase): From f2d061c9210b1495e9e420d7e4cab1dacc9c7adb Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Wed, 24 Jul 2019 22:42:36 -0700 Subject: [PATCH 42/77] Test poynting planes on both half-steps --- fdfd_tools/test_fdtd.py | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/fdfd_tools/test_fdtd.py b/fdfd_tools/test_fdtd.py index 879d3d4..15c5b4d 100644 --- a/fdfd_tools/test_fdtd.py +++ b/fdfd_tools/test_fdtd.py @@ -88,18 +88,19 @@ class BasicTests(): dxes = self.dxes if self.dxes is not None else tuple(tuple(numpy.ones(s) for s in self.epsilon.shape[1:]) for _ in range(2)) dV = numpy.prod(numpy.meshgrid(*dxes[0], indexing='ij'), axis=0) + mx = numpy.roll(self.src_mask, (-1, -1), axis=(0, 1)) + my = numpy.roll(self.src_mask, -1, axis=2) + mz = numpy.roll(self.src_mask, (+1, -1), axis=(0, 3)) + px = numpy.roll(self.src_mask, -1, axis=0) + py = self.src_mask.copy() + pz = numpy.roll(self.src_mask, +1, axis=0) + u_eprev = None for ii in range(1, 8): with self.subTest(i=ii): u_hstep = fdtd.energy_hstep(e0=self.es[ii-1], h1=self.hs[ii], e2=self.es[ii], **args) u_estep = fdtd.energy_estep(h0=self.hs[ii], e1=self.es[ii], h2=self.hs[ii + 1], **args) - mx = numpy.roll(self.src_mask, (-1, -1), axis=(0, 1)) - my = numpy.roll(self.src_mask, -1, axis=2) - mz = numpy.roll(self.src_mask, (+1, -1), axis=(0, 3)) - px = numpy.roll(self.src_mask, -1, axis=0) - py = self.src_mask.copy() - pz = numpy.roll(self.src_mask, +1, axis=0) s_h2e = -fdtd.poynting(e=self.es[ii], h=self.hs[ii]) * self.dt s_h2e[0] *= dxes[0][1][None, :, None] * dxes[0][2][None, None, :] s_h2e[1] *= dxes[0][0][:, None, None] * dxes[0][2][None, None, :] @@ -110,6 +111,23 @@ class BasicTests(): self.assertTrue(numpy.allclose(sum(planes), (u_estep - u_hstep)[self.src_mask[1]]), msg='planes: {} (sum: {})\n du:\n {}'.format(planes, sum(planes), (u_estep - u_hstep)[self.src_mask[1]])) + if u_eprev is None: + u_eprev = u_estep + continue + + s_e2h = -fdtd.poynting(e=self.es[ii - 1], h=self.hs[ii]) * self.dt + s_e2h[0] *= dxes[0][1][None, :, None] * dxes[0][2][None, None, :] + s_e2h[1] *= dxes[0][0][:, None, None] * dxes[0][2][None, None, :] + s_e2h[2] *= dxes[0][0][:, None, None] * dxes[0][1][None, :, None] + planes = [s_e2h[px].sum(), -s_e2h[mx].sum(), + s_e2h[py].sum(), -s_e2h[my].sum(), + s_e2h[pz].sum(), -s_e2h[mz].sum()] + self.assertTrue(numpy.allclose(sum(planes), (u_hstep - u_eprev)[self.src_mask[1]]), + msg='planes: {} (sum: {})\n du:\n {}'.format(planes, sum(planes), (u_hstep - u_eprev)[self.src_mask[1]])) + + # previous half-step + u_eprev = u_estep + class Basic2DNoDXOnlyVacuum(unittest.TestCase, BasicTests): def setUp(self): From 56a1349959c5f00d142a471ccb6603452140823a Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Thu, 1 Aug 2019 23:16:32 -0700 Subject: [PATCH 43/77] Add missing return --- fdfd_tools/waveguide_mode.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/fdfd_tools/waveguide_mode.py b/fdfd_tools/waveguide_mode.py index aac718d..398f2eb 100644 --- a/fdfd_tools/waveguide_mode.py +++ b/fdfd_tools/waveguide_mode.py @@ -425,6 +425,8 @@ def compute_source_wg(E: field_t, omega=omega, dxes=dxes, axis=axis, polarity=polarity, slices=slices4, epsilon=epsilon, mu=mu) + return J + def compute_overlap_ce(E: field_t, wavenumber: complex, From 1d9c9644ee6d243e0674b452c765bd6121ca9d2a Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Thu, 1 Aug 2019 23:17:13 -0700 Subject: [PATCH 44/77] input shouldn't be sliced with expanded slices --- fdfd_tools/waveguide_mode.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/fdfd_tools/waveguide_mode.py b/fdfd_tools/waveguide_mode.py index 398f2eb..2db9d68 100644 --- a/fdfd_tools/waveguide_mode.py +++ b/fdfd_tools/waveguide_mode.py @@ -479,7 +479,9 @@ def expand_wgmode_e(E: field_t, slices_exp[axis] = slice(E[0].shape[axis]) slices_exp = (slice(3), *slices_exp) - Ee[slices_exp] = phase_E * numpy.array(E)[slices_Exp] + slices_in = tuple(slice(3), *slices) + + Ee[slices_exp] = phase_E * numpy.array(E)[slices_in] return Ee From 1793e8cc3755ba12d85234a7169cb9b526390d6e Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Thu, 1 Aug 2019 23:48:25 -0700 Subject: [PATCH 45/77] move to 3xNxMxP arrays --- fdfd_tools/waveguide_mode.py | 63 ++++++++++++++---------------------- 1 file changed, 25 insertions(+), 38 deletions(-) diff --git a/fdfd_tools/waveguide_mode.py b/fdfd_tools/waveguide_mode.py index 2db9d68..5400f30 100644 --- a/fdfd_tools/waveguide_mode.py +++ b/fdfd_tools/waveguide_mode.py @@ -99,7 +99,7 @@ def solve_waveguide_mode(mode_number: int, :return: {'E': List[numpy.ndarray], 'H': List[numpy.ndarray], 'wavenumber': complex} """ if mu is None: - mu = [numpy.ones_like(epsilon[0])] * 3 + mu = numpy.ones_like(epsilon) slices = tuple(slices) @@ -131,18 +131,14 @@ def solve_waveguide_mode(mode_number: int, # Apply phase shift to H-field d_prop = 0.5 * sum(dxab_forward) - for a in range(3): - fields_2d['H'][a] *= numpy.exp(-polarity * 1j * 0.5 * fields_2d['wavenumber'] * d_prop) + fields_2d['H'] *= numpy.exp(-polarity * 1j * 0.5 * fields_2d['wavenumber'] * d_prop) # Expand E, H to full epsilon space we were given - E = [None]*3 - H = [None]*3 + E = numpy.zeros_like(epsilon, dtype=complex) + H = numpy.zeros_like(epsilon, dtype=complex) for a, o in enumerate(reverse_order): - E[a] = numpy.zeros_like(epsilon[0], dtype=complex) - H[a] = numpy.zeros_like(epsilon[0], dtype=complex) - - E[a][slices] = fields_2d['E'][o][:, :, None].transpose(reverse_order) - H[a][slices] = fields_2d['H'][o][:, :, None].transpose(reverse_order) + E[(a, *slices)] = fields_2d['E'][o][:, :, None].transpose(reverse_order) + H[(a, *slices)] = fields_2d['H'][o][:, :, None].transpose(reverse_order) results = { 'wavenumber': fields_2d['wavenumber'], @@ -180,27 +176,24 @@ def compute_source(E: field_t, :return: J distribution for the unidirectional source """ if mu is None: - mu = [1] * 3 + mu = numpy.ones(3) - J = [None]*3 - M = [None]*3 + J = numpy.zeros_like(E, dtype=complex) + M = numpy.zeros_like(E, dtype=complex) src_order = numpy.roll(range(3), -axis) exp_iphi = numpy.exp(1j * polarity * wavenumber * dxes[1][axis][slices[axis]]) - J[src_order[0]] = numpy.zeros_like(E[0]) J[src_order[1]] = +exp_iphi * H[src_order[2]] * polarity J[src_order[2]] = -exp_iphi * H[src_order[1]] * polarity rollby = -1 if polarity > 0 else 0 - M[src_order[0]] = numpy.zeros_like(E[0]) M[src_order[1]] = +numpy.roll(E[src_order[2]], rollby, axis=axis) M[src_order[2]] = -numpy.roll(E[src_order[1]], rollby, axis=axis) m2j = functional.m2j(omega, dxes, mu) Jm = m2j(M) - Jtot = [ji + jmi for ji, jmi in zip(J, Jm)] - + Jtot = J + Jm return Jtot @@ -236,8 +229,9 @@ def compute_overlap_e(E: field_t, """ slices = tuple(slices) - cross_plane = [slice(None)] * 3 - cross_plane[axis] = slices[axis] + cross_plane = [slice(None)] * 4 + cross_plane[axis + 1] = slices[axis] + cross_plane = tuple(cross_plane) # Determine phase factors for parallel slices a_shape = numpy.roll([-1, 1, 1], axis) @@ -248,11 +242,8 @@ def compute_overlap_e(E: field_t, phase_H = numpy.exp(iphi * (a_H - a_H[slices[axis]])).reshape(a_shape) # Expand our slice to the entire grid using the calculated phase factors - Ee = [None]*3 - He = [None]*3 - for k in range(3): - Ee[k] = phase_E * E[k][tuple(cross_plane)] - He[k] = phase_H * H[k][tuple(cross_plane)] + Ee = phase_E * E[cross_plane] + He = phase_H * H[cross_plane] # Write out the operator product for the mode orthogonality integral @@ -359,17 +350,16 @@ def compute_source_q(E: field_t, A2f = functional.curl_e(dxes) J = A1f(H) - M = A2f([-E[i] for i in range(3)]) + M = A2f(-E) m2j = functional.m2j(omega, dxes, mu) Jm = m2j(M) - Jtot = [ji + jmi for ji, jmi in zip(J, Jm)] + Jtot = J + Jm return Jtot, J, M - def compute_source_e(QE: field_t, omega: complex, dxes: dx_lists_t, @@ -389,16 +379,15 @@ def compute_source_e(QE: field_t, # Trim a cell from each end of the propagation axis slices_reduced = list(slices) slices_reduced[axis] = slice(slices[axis].start + 1, slices[axis].stop - 1) - slices_reduced = tuple(slices) + slices_reduced = tuple(slice(None), *slices) # Don't actually need to mask out E here since it needs to be pre-masked (QE) A = functional.e_full(omega, dxes, epsilon, mu) - J4 = [ji / (-1j * omega) for ji in A(QE)] #J4 is 4-cell result of -iwJ = A QE + J4 = A(QE) / (-1j * omega) #J4 is 4-cell result of -iwJ = A QE J = numpy.zeros_like(J4) - for a in range(3): - J[a][slices_reduced] = J4[a][slices_reduced] + J[slices_reduced] = J4[slices_reduced] return J @@ -445,14 +434,12 @@ def compute_overlap_ce(E: field_t, slices2 = list(slices) slices2[axis] = slice(start, stop) - slices2 = tuple(slices2) + slices2 = tuple(slice(None), slices2) Etgt = numpy.zeros_like(Ee) - for a in range(3): - Etgt[a][slices2] = Ee[a][slices2] + Etgt[slices2] = Ee[slices2] Etgt /= (Etgt.conj() * Etgt).sum() - return Etgt, slices2 @@ -476,10 +463,10 @@ def expand_wgmode_e(E: field_t, Ee = numpy.zeros_like(E) slices_exp = list(slices) - slices_exp[axis] = slice(E[0].shape[axis]) - slices_exp = (slice(3), *slices_exp) + slices_exp[axis] = slice(E.shape[axis + 1]) + slices_exp = (slice(None), *slices_exp) - slices_in = tuple(slice(3), *slices) + slices_in = tuple(slice(None), *slices) Ee[slices_exp] = phase_E * numpy.array(E)[slices_in] From 148930883770de8fa7272c3c49100dc8b3baabf2 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 3 Aug 2019 12:11:45 -0700 Subject: [PATCH 46/77] Comment capitalization fix --- fdfd_tools/waveguide_mode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fdfd_tools/waveguide_mode.py b/fdfd_tools/waveguide_mode.py index 5400f30..d12ffbf 100644 --- a/fdfd_tools/waveguide_mode.py +++ b/fdfd_tools/waveguide_mode.py @@ -370,7 +370,7 @@ def compute_source_e(QE: field_t, mu: field_t = None, ) -> field_t: """ - Want (AQ-QA) E = -iwj, where Q is a mask + Want (AQ-QA) E = -iwJ, where Q is a mask If E is an eigenmode, AE = 0 so just AQE = -iwJ Really only need E in 4 cells along axis (0, 0, Emode1, Emode2), find AE (1 fdtd step), then use center 2 cells as src """ From 06a491a96005874be4ba0c41a2a437824817e485 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sat, 3 Aug 2019 12:12:18 -0700 Subject: [PATCH 47/77] don't throw out our newly-reduced slices... --- fdfd_tools/waveguide_mode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fdfd_tools/waveguide_mode.py b/fdfd_tools/waveguide_mode.py index d12ffbf..0b839d8 100644 --- a/fdfd_tools/waveguide_mode.py +++ b/fdfd_tools/waveguide_mode.py @@ -379,7 +379,7 @@ def compute_source_e(QE: field_t, # Trim a cell from each end of the propagation axis slices_reduced = list(slices) slices_reduced[axis] = slice(slices[axis].start + 1, slices[axis].stop - 1) - slices_reduced = tuple(slice(None), *slices) + slices_reduced = tuple(slice(None), *slices_reduced) # Don't actually need to mask out E here since it needs to be pre-masked (QE) From 32055ec8d302ccb0d2187bdcfa686f32f7bfe192 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 4 Aug 2019 02:50:09 -0700 Subject: [PATCH 48/77] Use pytest for testing; generalize existing fdtd tests --- fdfd_tools/test/test_fdtd.py | 308 ++++++++++++++++++++++++++++++ fdfd_tools/test_fdtd.py | 353 ----------------------------------- 2 files changed, 308 insertions(+), 353 deletions(-) create mode 100644 fdfd_tools/test/test_fdtd.py delete mode 100644 fdfd_tools/test_fdtd.py diff --git a/fdfd_tools/test/test_fdtd.py b/fdfd_tools/test/test_fdtd.py new file mode 100644 index 0000000..53fe9f8 --- /dev/null +++ b/fdfd_tools/test/test_fdtd.py @@ -0,0 +1,308 @@ +import numpy +import pytest +import dataclasses +from typing import List, Tuple +from numpy.testing import assert_allclose, assert_array_equal + +from fdfd_tools import fdtd + + +prng = numpy.random.RandomState(12345) + +def assert_fields_close(a, b, *args, **kwargs): + numpy.testing.assert_allclose(a, b, verbose=False, err_msg='Fields did not match:\n{}\n{}'.format(numpy.rollaxis(a, -1), + numpy.rollaxis(b, -1)), *args, **kwargs) + +def assert_close(a, b, *args, **kwargs): + numpy.testing.assert_allclose(a, b, *args, **kwargs) + + +def test_initial_fields(sim): + # Make sure initial fields didn't change + e0 = sim.es[0] + h0 = sim.hs[0] + j0 = sim.js[0] + mask = (j0 != 0) + + assert_fields_close(e0[mask], j0[mask] / sim.epsilon[mask]) + assert not e0[~mask].any() + assert not h0.any() + + +def test_initial_energy(sim): + """ + Assumes fields start at 0 before J0 is added + """ + j0 = sim.js[0] + e0 = sim.es[0] + h0 = sim.hs[0] + h1 = sim.hs[1] + mask = (j0 != 0) + dV = numpy.prod(numpy.meshgrid(*sim.dxes[0], indexing='ij'), axis=0) + u0 = (j0 * j0.conj() / sim.epsilon * dV).sum(axis=0) + args = {'dxes': sim.dxes, + 'epsilon': sim.epsilon} + + # Make sure initial energy and E dot J are correct + energy0 = fdtd.energy_estep(h0=h0, e1=e0, h2=h1, **args) + e0_dot_j0 = fdtd.delta_energy_j(j0=j0, e1=e0, dxes=sim.dxes) + assert_fields_close(energy0, u0) + assert_fields_close(e0_dot_j0, u0) + + +def test_energy_conservation(sim): + """ + Assumes fields start at 0 before J0 is added + """ + e0 = sim.es[0] + j0 = sim.js[0] + u = fdtd.delta_energy_j(j0=j0, e1=e0, dxes=sim.dxes).sum() + args = {'dxes': sim.dxes, + 'epsilon': sim.epsilon} + + for ii in range(1, 8): + u_hstep = fdtd.energy_hstep(e0=sim.es[ii-1], h1=sim.hs[ii], e2=sim.es[ii], **args) + u_estep = fdtd.energy_estep(h0=sim.hs[ii], e1=sim.es[ii], h2=sim.hs[ii + 1], **args) + delta_j_A = fdtd.delta_energy_j(j0=sim.js[ii], e1=sim.es[ii-1], dxes=sim.dxes) + delta_j_B = fdtd.delta_energy_j(j0=sim.js[ii], e1=sim.es[ii], dxes=sim.dxes) + + u += delta_j_A.sum() + assert_close(u_hstep.sum(), u) + u += delta_j_B.sum() + assert_close(u_estep.sum(), u) + + +def test_poynting_divergence(sim): + args = {'dxes': sim.dxes, + 'epsilon': sim.epsilon} + + dV = numpy.prod(numpy.meshgrid(*sim.dxes[0], indexing='ij'), axis=0) + + u_eprev = None + for ii in range(1, 8): + u_hstep = fdtd.energy_hstep(e0=sim.es[ii-1], h1=sim.hs[ii], e2=sim.es[ii], **args) + u_estep = fdtd.energy_estep(h0=sim.hs[ii], e1=sim.es[ii], h2=sim.hs[ii + 1], **args) + delta_j_B = fdtd.delta_energy_j(j0=sim.js[ii], e1=sim.es[ii], dxes=sim.dxes) + + du_half_h2e = u_estep - u_hstep - delta_j_B + div_s_h2e = sim.dt * fdtd.poynting_divergence(e=sim.es[ii], h=sim.hs[ii], dxes=sim.dxes) * dV + assert_fields_close(du_half_h2e, -div_s_h2e, rtol=1e-4) + + if u_eprev is None: + u_eprev = u_estep + continue + + # previous half-step + delta_j_A = fdtd.delta_energy_j(j0=sim.js[ii], e1=sim.es[ii-1], dxes=sim.dxes) + + du_half_e2h = u_hstep - u_eprev - delta_j_A + div_s_e2h = sim.dt * fdtd.poynting_divergence(e=sim.es[ii-1], h=sim.hs[ii], dxes=sim.dxes) * dV + assert_fields_close(du_half_e2h, -div_s_e2h, rtol=1e-4) + u_eprev = u_estep + + +def test_poynting_planes(sim): + mask = (sim.js[0] != 0) + if mask.sum() > 1: + pytest.skip('test_poynting_planes can only test single point sources') + + args = {'dxes': sim.dxes, + 'epsilon': sim.epsilon} + dV = numpy.prod(numpy.meshgrid(*sim.dxes[0], indexing='ij'), axis=0) + + mx = numpy.roll(mask, (-1, -1), axis=(0, 1)) + my = numpy.roll(mask, -1, axis=2) + mz = numpy.roll(mask, (+1, -1), axis=(0, 3)) + px = numpy.roll(mask, -1, axis=0) + py = mask.copy() + pz = numpy.roll(mask, +1, axis=0) + + u_eprev = None + for ii in range(1, 8): + u_hstep = fdtd.energy_hstep(e0=sim.es[ii-1], h1=sim.hs[ii], e2=sim.es[ii], **args) + u_estep = fdtd.energy_estep(h0=sim.hs[ii], e1=sim.es[ii], h2=sim.hs[ii + 1], **args) + + s_h2e = -fdtd.poynting(e=sim.es[ii], h=sim.hs[ii]) * sim.dt + s_h2e[0] *= sim.dxes[0][1][None, :, None] * sim.dxes[0][2][None, None, :] + s_h2e[1] *= sim.dxes[0][0][:, None, None] * sim.dxes[0][2][None, None, :] + s_h2e[2] *= sim.dxes[0][0][:, None, None] * sim.dxes[0][1][None, :, None] + planes = [s_h2e[px].sum(), -s_h2e[mx].sum(), + s_h2e[py].sum(), -s_h2e[my].sum(), + s_h2e[pz].sum(), -s_h2e[mz].sum()] + assert_close(sum(planes), (u_estep - u_hstep).sum()) + if u_eprev is None: + u_eprev = u_estep + continue + + s_e2h = -fdtd.poynting(e=sim.es[ii - 1], h=sim.hs[ii]) * sim.dt + s_e2h[0] *= sim.dxes[0][1][None, :, None] * sim.dxes[0][2][None, None, :] + s_e2h[1] *= sim.dxes[0][0][:, None, None] * sim.dxes[0][2][None, None, :] + s_e2h[2] *= sim.dxes[0][0][:, None, None] * sim.dxes[0][1][None, :, None] + planes = [s_e2h[px].sum(), -s_e2h[mx].sum(), + s_e2h[py].sum(), -s_e2h[my].sum(), + s_e2h[pz].sum(), -s_e2h[mz].sum()] + assert_close(sum(planes), (u_hstep - u_eprev).sum()) + + # previous half-step + u_eprev = u_estep + +## Now tested elsewhere +#def test_j_dot_e(sim): +# for tt in sim.j_steps: +# e0 = sim.es[tt - 1] +# j1 = sim.js[tt] +# e1 = sim.es[tt] +# +# delta_j_A = fdtd.delta_energy_j(j0=j1, e1=e0, dxes=sim.dxes) +# delta_j_B = fdtd.delta_energy_j(j0=j1, e1=e1, dxes=sim.dxes) +# +# args = {'dxes': sim.dxes, +# 'epsilon': sim.epsilon} +# +# u_eprev = fdtd.energy_estep(h0=sim.hs[tt-1], e1=sim.es[tt-1], h2=sim.hs[tt], **args) +# u_hstep = fdtd.energy_hstep(e0=sim.es[tt-1], h1=sim.hs[tt], e2=sim.es[tt], **args) +# u_estep = fdtd.energy_estep(h0=sim.hs[tt], e1=sim.es[tt], h2=sim.hs[tt + 1], **args) +# +# assert_close(delta_j_A.sum(), (u_hstep - u_eprev).sum(), rtol=1e-4) +# assert_close(delta_j_B.sum(), (u_estep - u_hstep).sum(), rtol=1e-4) + + +@pytest.fixture(scope='module', + params=[(5, 5, 1), + (5, 1, 5), + (5, 5, 5), +# (7, 7, 7), + ]) +def shape(request): + yield (3, *request.param) + + +@pytest.fixture(scope='module', params=[0.3]) +def dt(request): + yield request.param + + +@pytest.fixture(scope='module', params=[1.0, 1.5]) +def epsilon_bg(request): + yield request.param + + +@pytest.fixture(scope='module', params=[1.0, 2.5]) +def epsilon_fg(request): + yield request.param + + +@pytest.fixture(scope='module', params=['center', '000', 'random']) +def epsilon(request, shape, epsilon_bg, epsilon_fg): + is3d = (numpy.array(shape) == 1).sum() == 0 + if is3d: + if request.param == '000': + pytest.skip('Skipping 000 epsilon because test is 3D (for speed)') + if epsilon_bg != 1: + pytest.skip('Skipping epsilon_bg != 1 because test is 3D (for speed)') + if epsilon_fg not in (1.0, 2.0): + pytest.skip('Skipping epsilon_fg not in (1, 2) because test is 3D (for speed)') + + epsilon = numpy.full(shape, epsilon_bg, dtype=float) + if request.param == 'center': + epsilon[:, shape[1]//2, shape[2]//2, shape[3]//2] = epsilon_fg + elif request.param == '000': + epsilon[:, 0, 0, 0] = epsilon_fg + elif request.param == 'random': + epsilon[:] = prng.uniform(low=min(epsilon_bg, epsilon_fg), + high=max(epsilon_bg, epsilon_fg), + size=shape) + + yield epsilon + + +@pytest.fixture(scope='module', params=[1.0])#, 1.5]) +def j_mag(request): + yield request.param + + +@pytest.fixture(scope='module', params=['center', 'random']) +def j_distribution(request, shape, j_mag): + j = numpy.zeros(shape) + if request.param == 'center': + j[:, shape[1]//2, shape[2]//2, shape[3]//2] = j_mag + elif request.param == '000': + j[:, 0, 0, 0] = j_mag + elif request.param == 'random': + j[:] = prng.uniform(low=-j_mag, high=j_mag, size=shape) + yield j + + +@pytest.fixture(scope='module', params=[1.0, 1.5]) +def dx(request): + yield request.param + + +@pytest.fixture(scope='module', params=['uniform']) +def dxes(request, shape, dx): + if request.param == 'uniform': + dxes = [[numpy.full(s, dx) for s in shape[1:]] for _ in range(2)] + yield dxes + + +@pytest.fixture(scope='module', + params=[(0,), + (0, 4, 8), + ] + ) +def j_steps(request): + yield request.param + + +@dataclasses.dataclass() +class SimResult: + shape: Tuple[int] + dt: float + dxes: List[List[numpy.ndarray]] + epsilon: numpy.ndarray + j_distribution: numpy.ndarray + j_steps: Tuple[int] + es: List[numpy.ndarray] = dataclasses.field(default_factory=list) + hs: List[numpy.ndarray] = dataclasses.field(default_factory=list) + js: List[numpy.ndarray] = dataclasses.field(default_factory=list) + + +@pytest.fixture(scope='module') +def sim(request, shape, epsilon, dxes, dt, j_distribution, j_steps): + is3d = (numpy.array(shape) == 1).sum() == 0 + if is3d: + if dt != 0.3: + pytest.skip('Skipping dt != 0.3 because test is 3D (for speed)') + + sim = SimResult( + shape=shape, + dt=dt, + dxes=dxes, + epsilon=epsilon, + j_distribution=j_distribution, + j_steps=j_steps, + ) + + e = numpy.zeros_like(epsilon) + h = numpy.zeros_like(epsilon) + + assert 0 in j_steps + j_zeros = numpy.zeros_like(j_distribution) + + eh2h = fdtd.maxwell_h(dt=dt, dxes=dxes) + eh2e = fdtd.maxwell_e(dt=dt, dxes=dxes) + for tt in range(10): + e = e.copy() + h = h.copy() + eh2h(e, h) + eh2e(e, h, epsilon) + if tt in j_steps: + e += j_distribution / epsilon + sim.js.append(j_distribution) + else: + sim.js.append(j_zeros) + sim.es.append(e) + sim.hs.append(h) + return sim + + diff --git a/fdfd_tools/test_fdtd.py b/fdfd_tools/test_fdtd.py deleted file mode 100644 index 15c5b4d..0000000 --- a/fdfd_tools/test_fdtd.py +++ /dev/null @@ -1,353 +0,0 @@ -import unittest -import numpy - -from fdfd_tools import fdtd - - -class BasicTests(): - def test_initial_fields(self): - # Make sure initial fields didn't change - e0 = self.es[0] - h0 = self.hs[0] - mask = self.src_mask - - self.assertEqual(e0[mask], self.j_mag / self.epsilon[mask]) - self.assertFalse(e0[~mask].any()) - self.assertFalse(h0.any()) - - - def test_initial_energy(self): - e0 = self.es[0] - h0 = self.hs[0] - h1 = self.hs[1] - mask = self.src_mask[1] - dxes = self.dxes if self.dxes is not None else tuple(tuple(numpy.ones(s) for s in e0.shape[1:]) for _ in range(2)) - dV = numpy.prod(numpy.meshgrid(*dxes[0], indexing='ij'), axis=0) - u0 = self.j_mag * self.j_mag / self.epsilon[self.src_mask] * dV[mask] - args = {'dxes': self.dxes, - 'epsilon': self.epsilon} - - # Make sure initial energy and E dot J are correct - energy0 = fdtd.energy_estep(h0=h0, e1=e0, h2=self.hs[1], **args) - e_dot_j_0 = fdtd.delta_energy_j(j0=(e0 - 0) * self.epsilon, e1=e0, dxes=self.dxes) - self.assertEqual(energy0[mask], u0) - self.assertFalse(energy0[~mask].any(), msg='energy0: {}'.format(energy0)) - self.assertEqual(e_dot_j_0[mask], u0) - self.assertFalse(e_dot_j_0[~mask].any(), msg='e_dot_j_0: {}'.format(e_dot_j_0)) - - - def test_energy_conservation(self): - e0 = self.es[0] - u0 = fdtd.delta_energy_j(j0=(e0 - 0) * self.epsilon, e1=e0, dxes=self.dxes).sum() - args = {'dxes': self.dxes, - 'epsilon': self.epsilon} - - for ii in range(1, 8): - with self.subTest(i=ii): - u_hstep = fdtd.energy_hstep(e0=self.es[ii-1], h1=self.hs[ii], e2=self.es[ii], **args) - u_estep = fdtd.energy_estep(h0=self.hs[ii], e1=self.es[ii], h2=self.hs[ii + 1], **args) - self.assertTrue(numpy.allclose(u_hstep.sum(), u0), msg='u_hstep: {}\n{}'.format(u_hstep.sum(), numpy.rollaxis(u_hstep, -1))) - self.assertTrue(numpy.allclose(u_estep.sum(), u0), msg='u_estep: {}\n{}'.format(u_estep.sum(), numpy.rollaxis(u_estep, -1))) - - - def test_poynting_divergence(self): - args = {'dxes': self.dxes, - 'epsilon': self.epsilon} - - dxes = self.dxes if self.dxes is not None else tuple(tuple(numpy.ones(s) for s in self.epsilon.shape[1:]) for _ in range(2)) - dV = numpy.prod(numpy.meshgrid(*dxes[0], indexing='ij'), axis=0) - - u_eprev = None - for ii in range(1, 8): - with self.subTest(i=ii): - u_hstep = fdtd.energy_hstep(e0=self.es[ii-1], h1=self.hs[ii], e2=self.es[ii], **args) - u_estep = fdtd.energy_estep(h0=self.hs[ii], e1=self.es[ii], h2=self.hs[ii + 1], **args) - - du_half_h2e = u_estep - u_hstep - div_s_h2e = self.dt * fdtd.poynting_divergence(e=self.es[ii], h=self.hs[ii], dxes=self.dxes) * dV - self.assertTrue(numpy.allclose(du_half_h2e, -div_s_h2e, rtol=1e-4), - msg='du_half_h2e\n{}\ndiv_s_h2e\n{}'.format(numpy.rollaxis(du_half_h2e, -1), - -numpy.rollaxis(div_s_h2e, -1))) - - if u_eprev is None: - u_eprev = u_estep - continue - - # previous half-step - du_half_e2h = u_hstep - u_eprev - div_s_e2h = self.dt * fdtd.poynting_divergence(e=self.es[ii-1], h=self.hs[ii], dxes=self.dxes) * dV - self.assertTrue(numpy.allclose(du_half_e2h, -div_s_e2h, rtol=1e-4), - msg='du_half_e2h\n{}\ndiv_s_e2h\n{}'.format(numpy.rollaxis(du_half_e2h, -1), - -numpy.rollaxis(div_s_e2h, -1))) - u_eprev = u_estep - - - def test_poynting_planes(self): - args = {'dxes': self.dxes, - 'epsilon': self.epsilon} - dxes = self.dxes if self.dxes is not None else tuple(tuple(numpy.ones(s) for s in self.epsilon.shape[1:]) for _ in range(2)) - dV = numpy.prod(numpy.meshgrid(*dxes[0], indexing='ij'), axis=0) - - mx = numpy.roll(self.src_mask, (-1, -1), axis=(0, 1)) - my = numpy.roll(self.src_mask, -1, axis=2) - mz = numpy.roll(self.src_mask, (+1, -1), axis=(0, 3)) - px = numpy.roll(self.src_mask, -1, axis=0) - py = self.src_mask.copy() - pz = numpy.roll(self.src_mask, +1, axis=0) - - u_eprev = None - for ii in range(1, 8): - with self.subTest(i=ii): - u_hstep = fdtd.energy_hstep(e0=self.es[ii-1], h1=self.hs[ii], e2=self.es[ii], **args) - u_estep = fdtd.energy_estep(h0=self.hs[ii], e1=self.es[ii], h2=self.hs[ii + 1], **args) - - s_h2e = -fdtd.poynting(e=self.es[ii], h=self.hs[ii]) * self.dt - s_h2e[0] *= dxes[0][1][None, :, None] * dxes[0][2][None, None, :] - s_h2e[1] *= dxes[0][0][:, None, None] * dxes[0][2][None, None, :] - s_h2e[2] *= dxes[0][0][:, None, None] * dxes[0][1][None, :, None] - planes = [s_h2e[px].sum(), -s_h2e[mx].sum(), - s_h2e[py].sum(), -s_h2e[my].sum(), - s_h2e[pz].sum(), -s_h2e[mz].sum()] - self.assertTrue(numpy.allclose(sum(planes), (u_estep - u_hstep)[self.src_mask[1]]), - msg='planes: {} (sum: {})\n du:\n {}'.format(planes, sum(planes), (u_estep - u_hstep)[self.src_mask[1]])) - - if u_eprev is None: - u_eprev = u_estep - continue - - s_e2h = -fdtd.poynting(e=self.es[ii - 1], h=self.hs[ii]) * self.dt - s_e2h[0] *= dxes[0][1][None, :, None] * dxes[0][2][None, None, :] - s_e2h[1] *= dxes[0][0][:, None, None] * dxes[0][2][None, None, :] - s_e2h[2] *= dxes[0][0][:, None, None] * dxes[0][1][None, :, None] - planes = [s_e2h[px].sum(), -s_e2h[mx].sum(), - s_e2h[py].sum(), -s_e2h[my].sum(), - s_e2h[pz].sum(), -s_e2h[mz].sum()] - self.assertTrue(numpy.allclose(sum(planes), (u_hstep - u_eprev)[self.src_mask[1]]), - msg='planes: {} (sum: {})\n du:\n {}'.format(planes, sum(planes), (u_hstep - u_eprev)[self.src_mask[1]])) - - # previous half-step - u_eprev = u_estep - - -class Basic2DNoDXOnlyVacuum(unittest.TestCase, BasicTests): - def setUp(self): - shape = [3, 5, 5, 1] - self.dt = 0.5 - self.epsilon = numpy.ones(shape, dtype=float) - self.j_mag = 32 - self.dxes = None - - self.src_mask = numpy.zeros_like(self.epsilon, dtype=bool) - self.src_mask[1, 2, 2, 0] = True - - e = numpy.zeros_like(self.epsilon) - h = numpy.zeros_like(self.epsilon) - e[self.src_mask] = self.j_mag / self.epsilon[self.src_mask] - self.es = [e] - self.hs = [h] - - eh2h = fdtd.maxwell_h(dt=self.dt, dxes=self.dxes) - eh2e = fdtd.maxwell_e(dt=self.dt, dxes=self.dxes) - for _ in range(9): - e = e.copy() - h = h.copy() - eh2h(e, h) - eh2e(e, h, self.epsilon) - self.es.append(e) - self.hs.append(h) - - -class Basic2DUniformDX3(unittest.TestCase, BasicTests): - def setUp(self): - shape = [3, 5, 5, 1] - self.dt = 0.5 - self.j_mag = 32 - self.dxes = tuple(tuple(numpy.full(s, 2.0) for s in shape[1:]) for _ in range(2)) - - self.src_mask = numpy.zeros(shape, dtype=bool) - self.src_mask[1, 2, 2, 0] = True - - self.epsilon = numpy.full(shape, 1, dtype=float) - self.epsilon[self.src_mask] = 2 - - e = numpy.zeros_like(self.epsilon) - h = numpy.zeros_like(self.epsilon) - e[self.src_mask] = self.j_mag / self.epsilon[self.src_mask] - self.es = [e] - self.hs = [h] - - eh2h = fdtd.maxwell_h(dt=self.dt, dxes=self.dxes) - eh2e = fdtd.maxwell_e(dt=self.dt, dxes=self.dxes) - for _ in range(9): - e = e.copy() - h = h.copy() - eh2h(e, h) - eh2e(e, h, self.epsilon) - self.es.append(e) - self.hs.append(h) - - -class Basic3DUniformDXOnlyVacuum(unittest.TestCase, BasicTests): - def setUp(self): - shape = [3, 5, 5, 5] - self.dt = 0.5 - self.epsilon = numpy.ones(shape, dtype=float) - self.j_mag = 32 - self.dxes = tuple(tuple(numpy.ones(s) for s in shape[1:]) for _ in range(2)) - - self.src_mask = numpy.zeros_like(self.epsilon, dtype=bool) - self.src_mask[1, 2, 2, 2] = True - - e = numpy.zeros_like(self.epsilon) - h = numpy.zeros_like(self.epsilon) - e[self.src_mask] = self.j_mag / self.epsilon[self.src_mask] - self.es = [e] - self.hs = [h] - - eh2h = fdtd.maxwell_h(dt=self.dt, dxes=self.dxes) - eh2e = fdtd.maxwell_e(dt=self.dt, dxes=self.dxes) - for _ in range(9): - e = e.copy() - h = h.copy() - eh2h(e, h) - eh2e(e, h, self.epsilon) - self.es.append(e) - self.hs.append(h) - - - -class Basic3DUniformDXUniformN(unittest.TestCase, BasicTests): - def setUp(self): - shape = [3, 5, 5, 5] - self.dt = 0.5 - self.epsilon = numpy.full(shape, 2, dtype=float) - self.j_mag = 32 - self.dxes = tuple(tuple(numpy.ones(s) for s in shape[1:]) for _ in range(2)) - - self.src_mask = numpy.zeros_like(self.epsilon, dtype=bool) - self.src_mask[1, 2, 2, 2] = True - - e = numpy.zeros_like(self.epsilon) - h = numpy.zeros_like(self.epsilon) - e[self.src_mask] = self.j_mag / self.epsilon[self.src_mask] - self.es = [e] - self.hs = [h] - - eh2h = fdtd.maxwell_h(dt=self.dt, dxes=self.dxes) - eh2e = fdtd.maxwell_e(dt=self.dt, dxes=self.dxes) - for _ in range(9): - e = e.copy() - h = h.copy() - eh2h(e, h) - eh2e(e, h, self.epsilon) - self.es.append(e) - self.hs.append(h) - - -class Basic3DUniformDX(unittest.TestCase, BasicTests): - def setUp(self): - shape = [3, 5, 5, 5] - self.dt = 0.33 - self.j_mag = 32 - self.dxes = tuple(tuple(numpy.ones(s) for s in shape[1:]) for _ in range(2)) - - self.src_mask = numpy.zeros(shape, dtype=bool) - self.src_mask[1, 2, 2, 2] = True - - self.epsilon = numpy.full(shape, 1, dtype=float) - self.epsilon[self.src_mask] = 2 - - e = numpy.zeros_like(self.epsilon) - h = numpy.zeros_like(self.epsilon) - e[self.src_mask] = self.j_mag / self.epsilon[self.src_mask] - self.es = [e] - self.hs = [h] - - eh2h = fdtd.maxwell_h(dt=self.dt, dxes=self.dxes) - eh2e = fdtd.maxwell_e(dt=self.dt, dxes=self.dxes) - for _ in range(9): - e = e.copy() - h = h.copy() - eh2h(e, h) - eh2e(e, h, self.epsilon) - self.es.append(e) - self.hs.append(h) - - -class Basic3DUniformDX3(unittest.TestCase, BasicTests): - def setUp(self): - shape = [3, 5, 5, 5] - self.dt = 0.5 - self.j_mag = 32 - self.dxes = tuple(tuple(numpy.full(s, 3.0) for s in shape[1:]) for _ in range(2)) - - self.src_mask = numpy.zeros(shape, dtype=bool) - self.src_mask[1, 2, 2, 2] = True - - self.epsilon = numpy.full(shape, 1, dtype=float) - self.epsilon[self.src_mask] = 2 - - e = numpy.zeros_like(self.epsilon) - h = numpy.zeros_like(self.epsilon) - e[self.src_mask] = self.j_mag / self.epsilon[self.src_mask] - self.es = [e] - self.hs = [h] - - eh2h = fdtd.maxwell_h(dt=self.dt, dxes=self.dxes) - eh2e = fdtd.maxwell_e(dt=self.dt, dxes=self.dxes) - for _ in range(9): - e = e.copy() - h = h.copy() - eh2h(e, h) - eh2e(e, h, self.epsilon) - self.es.append(e) - self.hs.append(h) - - -class JdotE_3DUniformDX(unittest.TestCase): - def setUp(self): - shape = [3, 5, 5, 5] - self.dt = 0.5 - self.j_mag = 32 - self.dxes = tuple(tuple(numpy.full(s, 2.0) for s in shape[1:]) for _ in range(2)) - - self.src_mask = numpy.zeros(shape, dtype=bool) - self.src_mask[1, 2, 2, 2] = True - - self.epsilon = numpy.full(shape, 4, dtype=float) - self.epsilon[self.src_mask] = 2 - - e = numpy.random.randint(-128, 128 + 1, size=shape).astype(float) - h = numpy.random.randint(-128, 128 + 1, size=shape).astype(float) - self.es = [e] - self.hs = [h] - - eh2h = fdtd.maxwell_h(dt=self.dt, dxes=self.dxes) - eh2e = fdtd.maxwell_e(dt=self.dt, dxes=self.dxes) - for ii in range(9): - e = e.copy() - h = h.copy() - eh2h(e, h) - eh2e(e, h, self.epsilon) - self.es.append(e) - self.hs.append(h) - - if ii == 1: - e[self.src_mask] += self.j_mag / self.epsilon[self.src_mask] - self.j_dot_e = self.j_mag * e[self.src_mask] - - - def test_j_dot_e(self): - e0 = self.es[2] - j0 = numpy.zeros_like(e0) - j0[self.src_mask] = self.j_mag - u0 = fdtd.delta_energy_j(j0=j0, e1=e0, dxes=self.dxes) - args = {'dxes': self.dxes, - 'epsilon': self.epsilon} - - ii=2 - u_hstep = fdtd.energy_hstep(e0=self.es[ii-1], h1=self.hs[ii], e2=self.es[ii], **args) - u_estep = fdtd.energy_estep(h0=self.hs[ii], e1=self.es[ii], h2=self.hs[ii + 1], **args) - #print(u0.sum(), (u_estep - u_hstep).sum()) - self.assertTrue(numpy.allclose(u0.sum(), (u_estep - u_hstep).sum(), rtol=1e-4)) - From 557a3b0d9c407f44c2a33a9de673c96852f26141 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 4 Aug 2019 02:53:04 -0700 Subject: [PATCH 49/77] Remove unused test code and tighten tolerances --- fdfd_tools/test/test_fdtd.py | 26 +++++--------------------- 1 file changed, 5 insertions(+), 21 deletions(-) diff --git a/fdfd_tools/test/test_fdtd.py b/fdfd_tools/test/test_fdtd.py index 53fe9f8..d74b8be 100644 --- a/fdfd_tools/test/test_fdtd.py +++ b/fdfd_tools/test/test_fdtd.py @@ -86,7 +86,7 @@ def test_poynting_divergence(sim): du_half_h2e = u_estep - u_hstep - delta_j_B div_s_h2e = sim.dt * fdtd.poynting_divergence(e=sim.es[ii], h=sim.hs[ii], dxes=sim.dxes) * dV - assert_fields_close(du_half_h2e, -div_s_h2e, rtol=1e-4) + assert_fields_close(du_half_h2e, -div_s_h2e) if u_eprev is None: u_eprev = u_estep @@ -97,7 +97,7 @@ def test_poynting_divergence(sim): du_half_e2h = u_hstep - u_eprev - delta_j_A div_s_e2h = sim.dt * fdtd.poynting_divergence(e=sim.es[ii-1], h=sim.hs[ii], dxes=sim.dxes) * dV - assert_fields_close(du_half_e2h, -div_s_e2h, rtol=1e-4) + assert_fields_close(du_half_e2h, -div_s_e2h) u_eprev = u_estep @@ -146,26 +146,10 @@ def test_poynting_planes(sim): # previous half-step u_eprev = u_estep -## Now tested elsewhere -#def test_j_dot_e(sim): -# for tt in sim.j_steps: -# e0 = sim.es[tt - 1] -# j1 = sim.js[tt] -# e1 = sim.es[tt] -# -# delta_j_A = fdtd.delta_energy_j(j0=j1, e1=e0, dxes=sim.dxes) -# delta_j_B = fdtd.delta_energy_j(j0=j1, e1=e1, dxes=sim.dxes) -# -# args = {'dxes': sim.dxes, -# 'epsilon': sim.epsilon} -# -# u_eprev = fdtd.energy_estep(h0=sim.hs[tt-1], e1=sim.es[tt-1], h2=sim.hs[tt], **args) -# u_hstep = fdtd.energy_hstep(e0=sim.es[tt-1], h1=sim.hs[tt], e2=sim.es[tt], **args) -# u_estep = fdtd.energy_estep(h0=sim.hs[tt], e1=sim.es[tt], h2=sim.hs[tt + 1], **args) -# -# assert_close(delta_j_A.sum(), (u_hstep - u_eprev).sum(), rtol=1e-4) -# assert_close(delta_j_B.sum(), (u_estep - u_hstep).sum(), rtol=1e-4) +##################################### +# Test fixtures +##################################### @pytest.fixture(scope='module', params=[(5, 5, 1), From 3d07969fd2a4ac40028dabfab54f020d94888662 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 4 Aug 2019 03:06:14 -0700 Subject: [PATCH 50/77] rename examples to avoid triggering pytest --- examples/{test.py => fdfd.py} | 0 examples/{test_fdtd.py => fdtd.py} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename examples/{test.py => fdfd.py} (100%) rename examples/{test_fdtd.py => fdtd.py} (100%) diff --git a/examples/test.py b/examples/fdfd.py similarity index 100% rename from examples/test.py rename to examples/fdfd.py diff --git a/examples/test_fdtd.py b/examples/fdtd.py similarity index 100% rename from examples/test_fdtd.py rename to examples/fdtd.py From 25cb83089dd9947344ee649efd0ea5179af7aa66 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 4 Aug 2019 03:06:32 -0700 Subject: [PATCH 51/77] modernize setup.py --- fdfd_tools/__init__.py | 1 + setup.py | 24 +++++++++++++++++++++--- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/fdfd_tools/__init__.py b/fdfd_tools/__init__.py index a4efa89..ecf4e15 100644 --- a/fdfd_tools/__init__.py +++ b/fdfd_tools/__init__.py @@ -23,3 +23,4 @@ from .vectorization import vec, unvec, field_t, vfield_t from .grid import dx_lists_t __author__ = 'Jan Petykiewicz' +version = '0.5' diff --git a/setup.py b/setup.py index ef1df08..2a64b37 100644 --- a/setup.py +++ b/setup.py @@ -1,18 +1,36 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 from setuptools import setup, find_packages +import fdfd_tools + +with open('README.md', 'r') as f: + long_description = f.read() setup(name='fdfd_tools', - version='0.4', + version=fdfd_tools.version, description='FDFD Electromagnetic simulation tools', + long_description=long_description, + long_description_content_type='text/markdown', author='Jan Petykiewicz', author_email='anewusername@gmail.com', - url='https://mpxd.net/gogs/jan/fdfd_tools', + url='https://mpxd.net/code/jan/fdfd_tools', packages=find_packages(), install_requires=[ 'numpy', 'scipy', ], extras_require={ + 'test': [ + 'pytest', + 'dataclasses', + ], }, + classifiers=[ + 'Programming Language :: Python :: 3', + 'Development Status :: 4 - Beta', + 'Intended Audience :: Developers', + 'Intended Audience :: Science/Research', + 'License :: OSI Approved :: GNU Affero General Public License v3', + 'Topic :: Scientific/Engineering :: Physics', + ], ) From f61bcf3dfa6874d09b5c4eecc9774b3cd6aaeb7c Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 4 Aug 2019 13:48:41 -0700 Subject: [PATCH 52/77] rename to meanas and split fdtd/fdfd --- README.md | 50 +-- fdfd_tools/__init__.py | 26 -- fdfd_tools/fdtd.py | 339 ------------------ meanas/__init__.py | 48 +++ {fdfd_tools => meanas}/eigensolvers.py | 0 {fdfd_tools => meanas/fdfd}/bloch.py | 0 {fdfd_tools => meanas/fdfd}/farfield.py | 0 {fdfd_tools => meanas/fdfd}/functional.py | 16 +- {fdfd_tools => meanas/fdfd}/operators.py | 30 +- fdfd_tools/grid.py => meanas/fdfd/scpml.py | 1 - {fdfd_tools => meanas/fdfd}/solvers.py | 2 +- {fdfd_tools => meanas/fdfd}/waveguide.py | 24 +- {fdfd_tools => meanas/fdfd}/waveguide_mode.py | 10 +- meanas/fdtd/__init__.py | 9 + meanas/fdtd/base.py | 87 +++++ meanas/fdtd/boundaries.py | 68 ++++ meanas/fdtd/energy.py | 84 +++++ meanas/fdtd/pml.py | 122 +++++++ {fdfd_tools => meanas}/test/test_fdtd.py | 2 +- meanas/types.py | 22 ++ {fdfd_tools => meanas}/vectorization.py | 10 +- setup.py | 8 +- 22 files changed, 519 insertions(+), 439 deletions(-) delete mode 100644 fdfd_tools/__init__.py delete mode 100644 fdfd_tools/fdtd.py create mode 100644 meanas/__init__.py rename {fdfd_tools => meanas}/eigensolvers.py (100%) rename {fdfd_tools => meanas/fdfd}/bloch.py (100%) rename {fdfd_tools => meanas/fdfd}/farfield.py (100%) rename {fdfd_tools => meanas/fdfd}/functional.py (87%) rename {fdfd_tools => meanas/fdfd}/operators.py (92%) rename fdfd_tools/grid.py => meanas/fdfd/scpml.py (99%) rename {fdfd_tools => meanas/fdfd}/solvers.py (97%) rename {fdfd_tools => meanas/fdfd}/waveguide.py (92%) rename {fdfd_tools => meanas/fdfd}/waveguide_mode.py (97%) create mode 100644 meanas/fdtd/__init__.py create mode 100644 meanas/fdtd/base.py create mode 100644 meanas/fdtd/boundaries.py create mode 100644 meanas/fdtd/energy.py create mode 100644 meanas/fdtd/pml.py rename {fdfd_tools => meanas}/test/test_fdtd.py (99%) create mode 100644 meanas/types.py rename {fdfd_tools => meanas}/vectorization.py (87%) diff --git a/README.md b/README.md index 5a3f49c..3ead0ca 100644 --- a/README.md +++ b/README.md @@ -1,46 +1,56 @@ -# fdfd_tools +# meanas -**fdfd_tools** is a python package containing utilities for -creating and analyzing 2D and 3D finite-difference frequency-domain (FDFD) -electromagnetic simulations. +**meanas** is a python package for electromagnetic simulations + +This package is intended for building simulation inputs, analyzing +simulation outputs, and running short simulations on unspecialized hardware. +It is designed to provide tooling and a baseline for other, high-performance +purpose- and hardware-specific solvers. **Contents** -* Library of sparse matrices for representing the electromagnetic wave - equation in 3D, as well as auxiliary matrices for conversion between fields -* Waveguide mode solver and waveguide mode operators -* Stretched-coordinate PML boundaries (SCPML) -* Functional versions of most operators -* Anisotropic media (eps_xx, eps_yy, eps_zz, mu_xx, ...) -* Arbitrary distributions of perfect electric and magnetic conductors (PEC / PMC) +- Finite difference frequency domain (FDFD) + * Library of sparse matrices for representing the electromagnetic wave + equation in 3D, as well as auxiliary matrices for conversion between fields + * Waveguide mode operators + * Waveguide mode eigensolver + * Stretched-coordinate PML boundaries (SCPML) + * Functional versions of most operators + * Anisotropic media (limited to diagonal elements eps_xx, eps_yy, eps_zz, mu_xx, ...) + * Arbitrary distributions of perfect electric and magnetic conductors (PEC / PMC) +- Finite difference time domain (FDTD) + * Basic Maxwell time-steps + * Poynting vector and energy calculation + * Convolutional PMLs This package does *not* provide a fast matrix solver, though by default -```fdfd_tools.solvers.generic(...)``` will call -```scipy.sparse.linalg.qmr(...)``` to perform a solve. -For 2D problems this should be fine; likewise, the waveguide mode +`meanas.fdfd.solvers.generic(...)` will call +`scipy.sparse.linalg.qmr(...)` to perform a solve. +For 2D FDFD problems this should be fine; likewise, the waveguide mode solver uses scipy's eigenvalue solver, with reasonable results. -For solving large (or 3D) problems, I recommend a GPU-based iterative -solver, such as [opencl_fdfd](https://mpxd.net/gogs/jan/opencl_fdfd) or +For solving large (or 3D) FDFD problems, I recommend a GPU-based iterative +solver, such as [opencl_fdfd](https://mpxd.net/code/jan/opencl_fdfd) or those included in [MAGMA](http://icl.cs.utk.edu/magma/index.html)). Your solver will need the ability to solve complex symmetric (non-Hermitian) linear systems, ideally with double precision. + ## Installation **Requirements:** -* python 3 (written and tested with 3.5) +* python 3 (tests require 3.7) * numpy * scipy Install with pip, via git: ```bash -pip install git+https://mpxd.net/gogs/jan/fdfd_tools.git@release +pip install git+https://mpxd.net/code/jan/meanas.git@release ``` ## Use -See examples/test.py for some simple examples; you may need additional -packages such as [gridlock](https://mpxd.net/gogs/jan/gridlock) +See `examples/` for some simple examples; you may need additional +packages such as [gridlock](https://mpxd.net/code/jan/gridlock) to run the examples. diff --git a/fdfd_tools/__init__.py b/fdfd_tools/__init__.py deleted file mode 100644 index ecf4e15..0000000 --- a/fdfd_tools/__init__.py +++ /dev/null @@ -1,26 +0,0 @@ -""" -Electromagnetic FDFD simulation tools - -Tools for 3D and 2D Electromagnetic Finite Difference Frequency Domain (FDFD) -simulations. These tools handle conversion of fields to/from vector form, -creation of the wave operator matrix, stretched-coordinate PMLs, PECs and PMCs, -field conversion operators, waveguide mode operator, and waveguide mode -solver. - -This package only contains a solver for the waveguide mode eigenproblem; -if you want to solve 3D problems you can use your favorite iterative sparse -matrix solver (so long as it can handle complex symmetric [non-Hermitian] -matrices, ideally with double precision). - - -Dependencies: -- numpy -- scipy - -""" - -from .vectorization import vec, unvec, field_t, vfield_t -from .grid import dx_lists_t - -__author__ = 'Jan Petykiewicz' -version = '0.5' diff --git a/fdfd_tools/fdtd.py b/fdfd_tools/fdtd.py deleted file mode 100644 index 15b5635..0000000 --- a/fdfd_tools/fdtd.py +++ /dev/null @@ -1,339 +0,0 @@ -from typing import List, Callable, Tuple, Dict -import numpy - -from . import dx_lists_t, field_t - -#TODO fix pmls - -__author__ = 'Jan Petykiewicz' - - -functional_matrix = Callable[[field_t], field_t] - - -def curl_h(dxes: dx_lists_t = None) -> functional_matrix: - """ - Curl operator for use with the H field. - - :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header - :return: Function for taking the discretized curl of the H-field, F(H) -> curlH - """ - if dxes: - dxyz_b = numpy.meshgrid(*dxes[1], indexing='ij') - - def dh(f, ax): - return (f - numpy.roll(f, 1, axis=ax)) / dxyz_b[ax] - else: - def dh(f, ax): - return f - numpy.roll(f, 1, axis=ax) - - def ch_fun(h: field_t) -> field_t: - output = numpy.empty_like(h) - output[0] = dh(h[2], 1) - output[1] = dh(h[0], 2) - output[2] = dh(h[1], 0) - output[0] -= dh(h[1], 2) - output[1] -= dh(h[2], 0) - output[2] -= dh(h[0], 1) - return output - - return ch_fun - - -def curl_e(dxes: dx_lists_t = None) -> functional_matrix: - """ - Curl operator for use with the E field. - - :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header - :return: Function for taking the discretized curl of the E-field, F(E) -> curlE - """ - if dxes is not None: - dxyz_a = numpy.meshgrid(*dxes[0], indexing='ij') - - def de(f, ax): - return (numpy.roll(f, -1, axis=ax) - f) / dxyz_a[ax] - else: - def de(f, ax): - return numpy.roll(f, -1, axis=ax) - f - - def ce_fun(e: field_t) -> field_t: - output = numpy.empty_like(e) - output[0] = de(e[2], 1) - output[1] = de(e[0], 2) - output[2] = de(e[1], 0) - output[0] -= de(e[1], 2) - output[1] -= de(e[2], 0) - output[2] -= de(e[0], 1) - return output - - return ce_fun - - -def maxwell_e(dt: float, dxes: dx_lists_t = None) -> functional_matrix: - curl_h_fun = curl_h(dxes) - - def me_fun(e: field_t, h: field_t, epsilon: field_t): - e += dt * curl_h_fun(h) / epsilon - return e - - return me_fun - - -def maxwell_h(dt: float, dxes: dx_lists_t = None) -> functional_matrix: - curl_e_fun = curl_e(dxes) - - def mh_fun(e: field_t, h: field_t): - h -= dt * curl_e_fun(e) - return h - - return mh_fun - - -def conducting_boundary(direction: int, - polarity: int - ) -> Tuple[functional_matrix, functional_matrix]: - dirs = [0, 1, 2] - if direction not in dirs: - raise Exception('Invalid direction: {}'.format(direction)) - dirs.remove(direction) - u, v = dirs - - if polarity < 0: - boundary_slice = [slice(None)] * 3 - shifted1_slice = [slice(None)] * 3 - boundary_slice[direction] = 0 - shifted1_slice[direction] = 1 - - def en(e: field_t): - e[direction][boundary_slice] = 0 - e[u][boundary_slice] = e[u][shifted1_slice] - e[v][boundary_slice] = e[v][shifted1_slice] - return e - - def hn(h: field_t): - h[direction][boundary_slice] = h[direction][shifted1_slice] - h[u][boundary_slice] = 0 - h[v][boundary_slice] = 0 - return h - - return en, hn - - elif polarity > 0: - boundary_slice = [slice(None)] * 3 - shifted1_slice = [slice(None)] * 3 - shifted2_slice = [slice(None)] * 3 - boundary_slice[direction] = -1 - shifted1_slice[direction] = -2 - shifted2_slice[direction] = -3 - - def ep(e: field_t): - e[direction][boundary_slice] = -e[direction][shifted2_slice] - e[direction][shifted1_slice] = 0 - e[u][boundary_slice] = e[u][shifted1_slice] - e[v][boundary_slice] = e[v][shifted1_slice] - return e - - def hp(h: field_t): - h[direction][boundary_slice] = h[direction][shifted1_slice] - h[u][boundary_slice] = -h[u][shifted2_slice] - h[u][shifted1_slice] = 0 - h[v][boundary_slice] = -h[v][shifted2_slice] - h[v][shifted1_slice] = 0 - return h - - return ep, hp - - else: - raise Exception('Bad polarity: {}'.format(polarity)) - - -def cpml(direction:int, - polarity: int, - dt: float, - epsilon: field_t, - thickness: int = 8, - ln_R_per_layer: float = -1.6, - epsilon_eff: float = 1, - mu_eff: float = 1, - m: float = 3.5, - ma: float = 1, - cfs_alpha: float = 0, - dtype: numpy.dtype = numpy.float32, - ) -> Tuple[Callable, Callable, Dict[str, field_t]]: - - if direction not in range(3): - raise Exception('Invalid direction: {}'.format(direction)) - - if polarity not in (-1, 1): - raise Exception('Invalid polarity: {}'.format(polarity)) - - if thickness <= 2: - raise Exception('It would be wise to have a pml with 4+ cells of thickness') - - if epsilon_eff <= 0: - raise Exception('epsilon_eff must be positive') - - sigma_max = -ln_R_per_layer / 2 * (m + 1) - kappa_max = numpy.sqrt(epsilon_eff * mu_eff) - alpha_max = cfs_alpha - transverse = numpy.delete(range(3), direction) - u, v = transverse - - xe = numpy.arange(1, thickness+1, dtype=float) - xh = numpy.arange(1, thickness+1, dtype=float) - if polarity > 0: - xe -= 0.5 - elif polarity < 0: - xh -= 0.5 - xe = xe[::-1] - xh = xh[::-1] - else: - raise Exception('Bad polarity!') - - expand_slice = [None] * 3 - expand_slice[direction] = slice(None) - - def par(x): - scaling = (x / thickness) ** m - sigma = scaling * sigma_max - kappa = 1 + scaling * (kappa_max - 1) - alpha = ((1 - x / thickness) ** ma) * alpha_max - p0 = numpy.exp(-(sigma / kappa + alpha) * dt) - p1 = sigma / (sigma + kappa * alpha) * (p0 - 1) - p2 = 1 / kappa - return p0[expand_slice], p1[expand_slice], p2[expand_slice] - - p0e, p1e, p2e = par(xe) - p0h, p1h, p2h = par(xh) - - region = [slice(None)] * 3 - if polarity < 0: - region[direction] = slice(None, thickness) - elif polarity > 0: - region[direction] = slice(-thickness, None) - else: - raise Exception('Bad polarity!') - - se = 1 if direction == 1 else -1 - - # TODO check if epsilon is uniform in pml region? - shape = list(epsilon[0].shape) - shape[direction] = thickness - psi_e = [numpy.zeros(shape, dtype=dtype), numpy.zeros(shape, dtype=dtype)] - psi_h = [numpy.zeros(shape, dtype=dtype), numpy.zeros(shape, dtype=dtype)] - - fields = { - 'psi_e_u': psi_e[0], - 'psi_e_v': psi_e[1], - 'psi_h_u': psi_h[0], - 'psi_h_v': psi_h[1], - } - - # Note that this is kinda slow -- would be faster to reuse dHv*p2h for the original - # H update, but then you have multiple arrays and a monolithic (field + pml) update operation - def pml_e(e: field_t, h: field_t, epsilon: field_t) -> Tuple[field_t, field_t]: - dHv = h[v][region] - numpy.roll(h[v], 1, axis=direction)[region] - dHu = h[u][region] - numpy.roll(h[u], 1, axis=direction)[region] - psi_e[0] *= p0e - psi_e[0] += p1e * dHv * p2e - psi_e[1] *= p0e - psi_e[1] += p1e * dHu * p2e - e[u][region] += se * dt / epsilon[u][region] * (psi_e[0] + (p2e - 1) * dHv) - e[v][region] -= se * dt / epsilon[v][region] * (psi_e[1] + (p2e - 1) * dHu) - return e, h - - def pml_h(e: field_t, h: field_t) -> Tuple[field_t, field_t]: - dEv = (numpy.roll(e[v], -1, axis=direction)[region] - e[v][region]) - dEu = (numpy.roll(e[u], -1, axis=direction)[region] - e[u][region]) - psi_h[0] *= p0h - psi_h[0] += p1h * dEv * p2h - psi_h[1] *= p0h - psi_h[1] += p1h * dEu * p2h - h[u][region] -= se * dt * (psi_h[0] + (p2h - 1) * dEv) - h[v][region] += se * dt * (psi_h[1] + (p2h - 1) * dEu) - return e, h - - return pml_e, pml_h, fields - - -def poynting(e, h): - s = (numpy.roll(e[1], -1, axis=0) * h[2] - numpy.roll(e[2], -1, axis=0) * h[1], - numpy.roll(e[2], -1, axis=1) * h[0] - numpy.roll(e[0], -1, axis=1) * h[2], - numpy.roll(e[0], -1, axis=2) * h[1] - numpy.roll(e[1], -1, axis=2) * h[0]) - return numpy.array(s) - - -def poynting_divergence(s=None, *, e=None, h=None, dxes=None): # TODO dxes - if dxes is None: - dxes = tuple(tuple(numpy.ones(1) for _ in range(3)) for _ in range(2)) - - if s is None: - s = poynting(e, h) - - ds = ((s[0] - numpy.roll(s[0], 1, axis=0)) / numpy.sqrt(dxes[0][0] * dxes[1][0])[:, None, None] + - (s[1] - numpy.roll(s[1], 1, axis=1)) / numpy.sqrt(dxes[0][1] * dxes[1][1])[None, :, None] + - (s[2] - numpy.roll(s[2], 1, axis=2)) / numpy.sqrt(dxes[0][2] * dxes[1][2])[None, None, :] ) - return ds - - -def energy_hstep(e0, h1, e2, epsilon=None, mu=None, dxes=None): - u = dxmul(e0 * e2, h1 * h1, epsilon, mu, dxes) - return u - - -def energy_estep(h0, e1, h2, epsilon=None, mu=None, dxes=None): - u = dxmul(e1 * e1, h0 * h2, epsilon, mu, dxes) - return u - - -def delta_energy_h2e(dt, e0, h1, e2, h3, epsilon=None, mu=None, dxes=None): - """ - This is just from (e2 * e2 + h3 * h1) - (h1 * h1 + e0 * e2) - """ - de = e2 * (e2 - e0) / dt - dh = h1 * (h3 - h1) / dt - du = dxmul(de, dh, epsilon, mu, dxes) - return du - - -def delta_energy_e2h(dt, h0, e1, h2, e3, epsilon=None, mu=None, dxes=None): - """ - This is just from (h2 * h2 + e3 * e1) - (e1 * e1 + h0 * h2) - """ - de = e1 * (e3 - e1) / dt - dh = h2 * (h2 - h0) / dt - du = dxmul(de, dh, epsilon, mu, dxes) - return du - - -def delta_energy_j(j0, e1, dxes=None): - if dxes is None: - dxes = tuple(tuple(numpy.ones(1) for _ in range(3)) for _ in range(2)) - - du = ((j0 * e1).sum(axis=0) * - dxes[0][0][:, None, None] * - dxes[0][1][None, :, None] * - dxes[0][2][None, None, :]) - return du - - -def dxmul(ee, hh, epsilon=None, mu=None, dxes=None): - if epsilon is None: - epsilon = 1 - if mu is None: - mu = 1 - if dxes is None: - dxes = tuple(tuple(numpy.ones(1) for _ in range(3)) for _ in range(2)) - - result = ((ee * epsilon).sum(axis=0) * - dxes[0][0][:, None, None] * - dxes[0][1][None, :, None] * - dxes[0][2][None, None, :] + - (hh * mu).sum(axis=0) * - dxes[1][0][:, None, None] * - dxes[1][1][None, :, None] * - dxes[1][2][None, None, :]) - return result - - - diff --git a/meanas/__init__.py b/meanas/__init__.py new file mode 100644 index 0000000..d4288a5 --- /dev/null +++ b/meanas/__init__.py @@ -0,0 +1,48 @@ +""" +Electromagnetic simulation tools + +This package is intended for building simulation inputs, analyzing +simulation outputs, and running short simulations on unspecialized hardware. +It is designed to provide tooling and a baseline for other, high-performance +purpose- and hardware-specific solvers. + + +**Contents** +- Finite difference frequency domain (FDFD) + * Library of sparse matrices for representing the electromagnetic wave + equation in 3D, as well as auxiliary matrices for conversion between fields + * Waveguide mode operators + * Waveguide mode eigensolver + * Stretched-coordinate PML boundaries (SCPML) + * Functional versions of most operators + * Anisotropic media (limited to diagonal elements eps_xx, eps_yy, eps_zz, mu_xx, ...) + * Arbitrary distributions of perfect electric and magnetic conductors (PEC / PMC) +- Finite difference time domain (FDTD) + * Basic Maxwell time-steps + * Poynting vector and energy calculation + * Convolutional PMLs + +This package does *not* provide a fast matrix solver, though by default +```meanas.fdfd.solvers.generic(...)``` will call +```scipy.sparse.linalg.qmr(...)``` to perform a solve. +For 2D FDFD problems this should be fine; likewise, the waveguide mode +solver uses scipy's eigenvalue solver, with reasonable results. + +For solving large (or 3D) FDFD problems, I recommend a GPU-based iterative +solver, such as [opencl_fdfd](https://mpxd.net/code/jan/opencl_fdfd) or +those included in [MAGMA](http://icl.cs.utk.edu/magma/index.html)). Your +solver will need the ability to solve complex symmetric (non-Hermitian) +linear systems, ideally with double precision. + + +Dependencies: +- numpy +- scipy + +""" + +from .types import dx_lists_t, field_t, vfield_t, field_updater +from .vectorization import vec, unvec + +__author__ = 'Jan Petykiewicz' +version = '0.5' diff --git a/fdfd_tools/eigensolvers.py b/meanas/eigensolvers.py similarity index 100% rename from fdfd_tools/eigensolvers.py rename to meanas/eigensolvers.py diff --git a/fdfd_tools/bloch.py b/meanas/fdfd/bloch.py similarity index 100% rename from fdfd_tools/bloch.py rename to meanas/fdfd/bloch.py diff --git a/fdfd_tools/farfield.py b/meanas/fdfd/farfield.py similarity index 100% rename from fdfd_tools/farfield.py rename to meanas/fdfd/farfield.py diff --git a/fdfd_tools/functional.py b/meanas/fdfd/functional.py similarity index 87% rename from fdfd_tools/functional.py rename to meanas/fdfd/functional.py index 1d39d84..e57fe88 100644 --- a/fdfd_tools/functional.py +++ b/meanas/fdfd/functional.py @@ -2,8 +2,8 @@ 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 inputs in the form E = [E_x, E_y, E_z], where each - component E_* is an ndarray of equal shape. +The functions generated here expect field inputs with shape (3, X, Y, Z), +e.g. E = [E_x, E_y, E_z] where each component has shape (X, Y, Z) """ from typing import List, Callable import numpy @@ -20,7 +20,7 @@ def curl_h(dxes: dx_lists_t) -> functional_matrix: """ Curl operator for use with the H field. - :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header + :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types :return: Function for taking the discretized curl of the H-field, F(H) -> curlH """ dxyz_b = numpy.meshgrid(*dxes[1], indexing='ij') @@ -41,7 +41,7 @@ def curl_e(dxes: dx_lists_t) -> functional_matrix: """ Curl operator for use with the E field. - :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header + :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types :return: Function for taking the discretized curl of the E-field, F(E) -> curlE """ dxyz_a = numpy.meshgrid(*dxes[0], indexing='ij') @@ -69,7 +69,7 @@ def e_full(omega: complex, (del x (1/mu * del x) - omega**2 * epsilon) E = -i * omega * J :param omega: Angular frequency of the simulation - :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header + :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types :param epsilon: Dielectric constant :param mu: Magnetic permeability (default 1 everywhere) :return: Function implementing the wave operator A(E) -> E @@ -100,7 +100,7 @@ def eh_full(omega: complex, Wave operator for full (both E and H) field representation. :param omega: Angular frequency of the simulation - :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header + :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types :param epsilon: Dielectric constant :param mu: Magnetic permeability (default 1 everywhere) :return: Function implementing the wave operator A(E, H) -> (E, H) @@ -131,7 +131,7 @@ def e2h(omega: complex, For use with e_full -- assumes that there is no magnetic current M. :param omega: Angular frequency of the simulation - :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header + :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types :param mu: Magnetic permeability (default 1 everywhere) :return: Function for converting E to H """ @@ -159,7 +159,7 @@ def m2j(omega: complex, For use with e.g. e_full(). :param omega: Angular frequency of the simulation - :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header + :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types :param mu: Magnetic permeability (default 1 everywhere) :return: Function for converting M to J """ diff --git a/fdfd_tools/operators.py b/meanas/fdfd/operators.py similarity index 92% rename from fdfd_tools/operators.py rename to meanas/fdfd/operators.py index 8809d09..3b2de68 100644 --- a/fdfd_tools/operators.py +++ b/meanas/fdfd/operators.py @@ -3,17 +3,13 @@ Sparse matrix operators for use with electromagnetic wave equations. These functions return sparse-matrix (scipy.sparse.spmatrix) representations of a variety of operators, intended for use with E and H fields vectorized using the - fdfd_tools.vec() and .unvec() functions (column-major/Fortran ordering). + meanas.vec() and .unvec() functions (column-major/Fortran ordering). E- and H-field values are defined on a Yee cell; epsilon values should be calculated for cells centered at each E component (mu at each H component). -Many of these functions require a 'dxes' parameter, of type fdfd_tools.dx_lists_type, - which contains grid cell width information in the following format: - [[[dx_e_0, dx_e_1, ...], [dy_e_0, ...], [dz_e_0, ...]], - [[dx_h_0, dx_h_1, ...], [dy_h_0, ...], [dz_h_0, ...]]] - where dx_e_0 is the x-width of the x=0 cells, as used when calculating dE/dx, - and dy_h_0 is the y-width of the y=0 cells, as used when calculating dH/dy, etc. +Many of these functions require a 'dxes' parameter, of type meanas.dx_lists_type; see +the meanas.types submodule for details. The following operators are included: @@ -57,7 +53,7 @@ def e_full(omega: complex, To make this matrix symmetric, use the preconditions from e_full_preconditioners(). :param omega: Angular frequency of the simulation - :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header + :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types :param epsilon: Vectorized dielectric constant :param mu: Vectorized magnetic permeability (default 1 everywhere). :param pec: Vectorized mask specifying PEC cells. Any cells where pec != 0 are interpreted @@ -101,7 +97,7 @@ def e_full_preconditioners(dxes: dx_lists_t The preconditioner matrices are diagonal and complex, with Pr = 1 / Pl - :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header + :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types :return: Preconditioner matrices (Pl, Pr) """ p_squared = [dxes[0][0][:, None, None] * dxes[1][1][None, :, None] * dxes[1][2][None, None, :], @@ -127,7 +123,7 @@ def h_full(omega: complex, (del x (1/epsilon * del x) - omega**2 * mu) H = i * omega * M :param omega: Angular frequency of the simulation - :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header + :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types :param epsilon: Vectorized dielectric constant :param mu: Vectorized magnetic permeability (default 1 everywhere) :param pec: Vectorized mask specifying PEC cells. Any cells where pec != 0 are interpreted @@ -177,7 +173,7 @@ def eh_full(omega: complex, for use with a field vector of the form hstack(vec(E), vec(H)). :param omega: Angular frequency of the simulation - :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header + :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types :param epsilon: Vectorized dielectric constant :param mu: Vectorized magnetic permeability (default 1 everywhere) :param pec: Vectorized mask specifying PEC cells. Any cells where pec != 0 are interpreted @@ -216,7 +212,7 @@ def curl_h(dxes: dx_lists_t) -> sparse.spmatrix: """ Curl operator for use with the H field. - :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header + :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types :return: Sparse matrix for taking the discretized curl of the H-field """ return cross(deriv_back(dxes[1])) @@ -226,7 +222,7 @@ def curl_e(dxes: dx_lists_t) -> sparse.spmatrix: """ Curl operator for use with the E field. - :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header + :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types :return: Sparse matrix for taking the discretized curl of the E-field """ return cross(deriv_forward(dxes[0])) @@ -242,7 +238,7 @@ def e2h(omega: complex, For use with e_full -- assumes that there is no magnetic current M. :param omega: Angular frequency of the simulation - :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header + :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types :param mu: Vectorized magnetic permeability (default 1 everywhere) :param pmc: Vectorized mask specifying PMC cells. Any cells where pmc != 0 are interpreted as containing a perfect magnetic conductor (PMC). @@ -270,7 +266,7 @@ def m2j(omega: complex, For use with eg. e_full. :param omega: Angular frequency of the simulation - :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header + :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types :param mu: Vectorized magnetic permeability (default 1 everywhere) :return: Sparse matrix for converting E to H """ @@ -454,7 +450,7 @@ def poynting_e_cross(e: vfield_t, dxes: dx_lists_t) -> sparse.spmatrix: Operator for computing the Poynting vector, containing the (E x) portion of the Poynting vector. :param e: Vectorized E-field for the ExH cross product - :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header + :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types :return: Sparse matrix containing (E x) portion of Poynting cross product """ shape = [len(dx) for dx in dxes[0]] @@ -483,7 +479,7 @@ def poynting_h_cross(h: vfield_t, dxes: dx_lists_t) -> sparse.spmatrix: Operator for computing the Poynting vector, containing the (H x) portion of the Poynting vector. :param h: Vectorized H-field for the HxE cross product - :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header + :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types :return: Sparse matrix containing (H x) portion of Poynting cross product """ shape = [len(dx) for dx in dxes[0]] diff --git a/fdfd_tools/grid.py b/meanas/fdfd/scpml.py similarity index 99% rename from fdfd_tools/grid.py rename to meanas/fdfd/scpml.py index 8ecb44f..c4091a0 100644 --- a/fdfd_tools/grid.py +++ b/meanas/fdfd/scpml.py @@ -8,7 +8,6 @@ import numpy __author__ = 'Jan Petykiewicz' -dx_lists_t = List[List[numpy.ndarray]] s_function_type = Callable[[float], float] diff --git a/fdfd_tools/solvers.py b/meanas/fdfd/solvers.py similarity index 97% rename from fdfd_tools/solvers.py rename to meanas/fdfd/solvers.py index 066725c..a0ce403 100644 --- a/fdfd_tools/solvers.py +++ b/meanas/fdfd/solvers.py @@ -70,7 +70,7 @@ def generic(omega: complex, """ Conjugate gradient FDFD solver using CSR sparse matrices. - All ndarray arguments should be 1D array, as returned by fdfd_tools.vec(). + All ndarray arguments should be 1D array, as returned by meanas.vec(). :param omega: Complex frequency to solve at. :param dxes: [[dx_e, dy_e, dz_e], [dx_h, dy_h, dz_h]] (complex cell sizes) diff --git a/fdfd_tools/waveguide.py b/meanas/fdfd/waveguide.py similarity index 92% rename from fdfd_tools/waveguide.py rename to meanas/fdfd/waveguide.py index 89e0d9c..48a1510 100644 --- a/fdfd_tools/waveguide.py +++ b/meanas/fdfd/waveguide.py @@ -51,7 +51,7 @@ def operator(omega: complex, z-dependence is assumed for the fields). :param omega: The angular frequency of the system - :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header (2D) + :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types (2D) :param epsilon: Vectorized dielectric constant grid :param mu: Vectorized magnetic permeability grid (default 1 everywhere) :return: Sparse matrix representation of the operator @@ -91,7 +91,7 @@ def normalized_fields(v: numpy.ndarray, :param v: Vector containing H_x and H_y fields :param wavenumber: Wavenumber satisfying A @ v == wavenumber**2 * v :param omega: The angular frequency of the system - :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header (2D) + :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types (2D) :param epsilon: Vectorized dielectric constant grid :param mu: Vectorized magnetic permeability grid (default 1 everywhere) :return: Normalized, vectorized (e, h) containing all vector components. @@ -120,6 +120,8 @@ def normalized_fields(v: numpy.ndarray, # Try to break symmetry to assign a consistent sign [experimental] E_weighted = unvec(e * energy * numpy.exp(1j * norm_angle), shape) sign = numpy.sign(E_weighted[:, :max(shape[0]//2, 1), :max(shape[1]//2, 1)].real.sum()) + logger.debug('norm_angle = {}'.format(norm_angle)) + logger.debug('norm_sign = {}'.format(sign) norm_factor = sign * norm_amplitude * numpy.exp(1j * norm_angle) @@ -140,7 +142,7 @@ def v2h(v: numpy.ndarray, :param v: Vector containing H_x and H_y fields :param wavenumber: Wavenumber satisfying A @ v == wavenumber**2 * v - :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header (2D) + :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types (2D) :param mu: Vectorized magnetic permeability grid (default 1 everywhere) :return: Vectorized H field with all vector components """ @@ -172,7 +174,7 @@ def v2e(v: numpy.ndarray, :param v: Vector containing H_x and H_y fields :param wavenumber: Wavenumber satisfying A @ v == wavenumber**2 * v :param omega: The angular frequency of the system - :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header (2D) + :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types (2D) :param epsilon: Vectorized dielectric constant grid :param mu: Vectorized magnetic permeability grid (default 1 everywhere) :return: Vectorized E field with all vector components. @@ -192,7 +194,7 @@ def e2h(wavenumber: complex, :param wavenumber: Wavenumber satisfying A @ v == wavenumber**2 * v :param omega: The angular frequency of the system - :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header (2D) + :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types (2D) :param mu: Vectorized magnetic permeability grid (default 1 everywhere) :return: Sparse matrix representation of the operator """ @@ -213,7 +215,7 @@ def h2e(wavenumber: complex, :param wavenumber: Wavenumber satisfying A @ v == wavenumber**2 * v :param omega: The angular frequency of the system - :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header (2D) + :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types (2D) :param epsilon: Vectorized dielectric constant grid :return: Sparse matrix representation of the operator """ @@ -226,7 +228,7 @@ def curl_e(wavenumber: complex, dxes: dx_lists_t) -> sparse.spmatrix: Discretized curl operator for use with the waveguide E field. :param wavenumber: Wavenumber satisfying A @ v == wavenumber**2 * v - :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header (2D) + :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types (2D) :return: Sparse matrix representation of the operator """ n = 1 @@ -243,7 +245,7 @@ def curl_h(wavenumber: complex, dxes: dx_lists_t) -> sparse.spmatrix: Discretized curl operator for use with the waveguide H field. :param wavenumber: Wavenumber satisfying A @ v == wavenumber**2 * v - :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header (2D) + :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types (2D) :return: Sparse matrix representation of the operator """ n = 1 @@ -268,7 +270,7 @@ def h_err(h: vfield_t, :param h: Vectorized H field :param wavenumber: Wavenumber satisfying A @ v == wavenumber**2 * v :param omega: The angular frequency of the system - :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header (2D) + :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types (2D) :param epsilon: Vectorized dielectric constant grid :param mu: Vectorized magnetic permeability grid (default 1 everywhere) :return: Relative error norm(OP @ h) / norm(h) @@ -299,7 +301,7 @@ def e_err(e: vfield_t, :param e: Vectorized E field :param wavenumber: Wavenumber satisfying A @ v == wavenumber**2 * v :param omega: The angular frequency of the system - :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header (2D) + :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types (2D) :param epsilon: Vectorized dielectric constant grid :param mu: Vectorized magnetic permeability grid (default 1 everywhere) :return: Relative error norm(OP @ e) / norm(e) @@ -335,7 +337,7 @@ def cylindrical_operator(omega: complex, theta-dependence is assumed for the fields). :param omega: The angular frequency of the system - :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header (2D) + :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types (2D) :param epsilon: Vectorized dielectric constant grid :param r0: Radius of curvature for the simulation. This should be the minimum value of r within the simulation domain. diff --git a/fdfd_tools/waveguide_mode.py b/meanas/fdfd/waveguide_mode.py similarity index 97% rename from fdfd_tools/waveguide_mode.py rename to meanas/fdfd/waveguide_mode.py index 0b839d8..6e2b192 100644 --- a/fdfd_tools/waveguide_mode.py +++ b/meanas/fdfd/waveguide_mode.py @@ -19,7 +19,7 @@ def solve_waveguide_mode_2d(mode_number: int, :param mode_number: Number of the mode, 0-indexed. :param omega: Angular frequency of the simulation - :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header + :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types :param epsilon: Dielectric constant :param mu: Magnetic permeability (default 1 everywhere) :param wavenumber_correction: Whether to correct the wavenumber to @@ -87,7 +87,7 @@ def solve_waveguide_mode(mode_number: int, :param mode_number: Number of the mode, 0-indexed :param omega: Angular frequency of the simulation - :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header + :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types :param axis: Propagation axis (0=x, 1=y, 2=z) :param polarity: Propagation direction (+1 for +ve, -1 for -ve) :param slices: epsilon[tuple(slices)] is used to select the portion of the grid to use @@ -167,7 +167,7 @@ def compute_source(E: field_t, :param H: H-field of the mode (advanced by half of a Yee cell from E) :param wavenumber: Wavenumber of the mode :param omega: Angular frequency of the simulation - :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header + :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types :param axis: Propagation axis (0=x, 1=y, 2=z) :param polarity: Propagation direction (+1 for +ve, -1 for -ve) :param slices: epsilon[tuple(slices)] is used to select the portion of the grid to use @@ -219,7 +219,7 @@ def compute_overlap_e(E: field_t, :param H: H-field of the mode (advanced by half of a Yee cell from E) :param wavenumber: Wavenumber of the mode :param omega: Angular frequency of the simulation - :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header + :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types :param axis: Propagation axis (0=x, 1=y, 2=z) :param polarity: Propagation direction (+1 for +ve, -1 for -ve) :param slices: epsilon[tuple(slices)] is used to select the portion of the grid to use @@ -283,7 +283,7 @@ def solve_waveguide_mode_cylindrical(mode_number: int, :param mode_number: Number of the mode, 0-indexed :param omega: Angular frequency of the simulation - :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header. + :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types. The first coordinate is assumed to be r, the second is y. :param epsilon: Dielectric constant :param r0: Radius of curvature for the simulation. This should be the minimum value of diff --git a/meanas/fdtd/__init__.py b/meanas/fdtd/__init__.py new file mode 100644 index 0000000..a1d278a --- /dev/null +++ b/meanas/fdtd/__init__.py @@ -0,0 +1,9 @@ +""" +Basic FDTD functionality +""" + +from .base import maxwell_e, maxwell_h +from .pml import cpml +from .energy import (poynting, poynting_divergence, energy_hstep, energy_estep, + delta_energy_h2e, delta_energy_h2e, delta_energy_j) +from .boundaries import conducting_boundary diff --git a/meanas/fdtd/base.py b/meanas/fdtd/base.py new file mode 100644 index 0000000..8dd1df3 --- /dev/null +++ b/meanas/fdtd/base.py @@ -0,0 +1,87 @@ +""" +Basic FDTD field updates +""" +from typing import List, Callable, Tuple, Dict +import numpy + +from .. import dx_lists_t, field_t, field_updater + +__author__ = 'Jan Petykiewicz' + + +def curl_h(dxes: dx_lists_t = None) -> field_updater: + """ + Curl operator for use with the H field. + + :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header + :return: Function for taking the discretized curl of the H-field, F(H) -> curlH + """ + if dxes: + dxyz_b = numpy.meshgrid(*dxes[1], indexing='ij') + + def dh(f, ax): + return (f - numpy.roll(f, 1, axis=ax)) / dxyz_b[ax] + else: + def dh(f, ax): + return f - numpy.roll(f, 1, axis=ax) + + def ch_fun(h: field_t) -> field_t: + output = numpy.empty_like(h) + output[0] = dh(h[2], 1) + output[1] = dh(h[0], 2) + output[2] = dh(h[1], 0) + output[0] -= dh(h[1], 2) + output[1] -= dh(h[2], 0) + output[2] -= dh(h[0], 1) + return output + + return ch_fun + + +def curl_e(dxes: dx_lists_t = None) -> field_updater: + """ + Curl operator for use with the E field. + + :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header + :return: Function for taking the discretized curl of the E-field, F(E) -> curlE + """ + if dxes is not None: + dxyz_a = numpy.meshgrid(*dxes[0], indexing='ij') + + def de(f, ax): + return (numpy.roll(f, -1, axis=ax) - f) / dxyz_a[ax] + else: + def de(f, ax): + return numpy.roll(f, -1, axis=ax) - f + + def ce_fun(e: field_t) -> field_t: + output = numpy.empty_like(e) + output[0] = de(e[2], 1) + output[1] = de(e[0], 2) + output[2] = de(e[1], 0) + output[0] -= de(e[1], 2) + output[1] -= de(e[2], 0) + output[2] -= de(e[0], 1) + return output + + return ce_fun + + +def maxwell_e(dt: float, dxes: dx_lists_t = None) -> field_updater: + curl_h_fun = curl_h(dxes) + + def me_fun(e: field_t, h: field_t, epsilon: field_t): + e += dt * curl_h_fun(h) / epsilon + return e + + return me_fun + + +def maxwell_h(dt: float, dxes: dx_lists_t = None) -> field_updater: + curl_e_fun = curl_e(dxes) + + def mh_fun(e: field_t, h: field_t): + h -= dt * curl_e_fun(e) + return h + + return mh_fun diff --git a/meanas/fdtd/boundaries.py b/meanas/fdtd/boundaries.py new file mode 100644 index 0000000..34a8d4a --- /dev/null +++ b/meanas/fdtd/boundaries.py @@ -0,0 +1,68 @@ +""" +Boundary conditions +""" + +from typing import List, Callable, Tuple, Dict +import numpy + +from .. import dx_lists_t, field_t, field_updater + + +def conducting_boundary(direction: int, + polarity: int + ) -> Tuple[field_updater, field_updater]: + dirs = [0, 1, 2] + if direction not in dirs: + raise Exception('Invalid direction: {}'.format(direction)) + dirs.remove(direction) + u, v = dirs + + if polarity < 0: + boundary_slice = [slice(None)] * 3 + shifted1_slice = [slice(None)] * 3 + boundary_slice[direction] = 0 + shifted1_slice[direction] = 1 + + def en(e: field_t): + e[direction][boundary_slice] = 0 + e[u][boundary_slice] = e[u][shifted1_slice] + e[v][boundary_slice] = e[v][shifted1_slice] + return e + + def hn(h: field_t): + h[direction][boundary_slice] = h[direction][shifted1_slice] + h[u][boundary_slice] = 0 + h[v][boundary_slice] = 0 + return h + + return en, hn + + elif polarity > 0: + boundary_slice = [slice(None)] * 3 + shifted1_slice = [slice(None)] * 3 + shifted2_slice = [slice(None)] * 3 + boundary_slice[direction] = -1 + shifted1_slice[direction] = -2 + shifted2_slice[direction] = -3 + + def ep(e: field_t): + e[direction][boundary_slice] = -e[direction][shifted2_slice] + e[direction][shifted1_slice] = 0 + e[u][boundary_slice] = e[u][shifted1_slice] + e[v][boundary_slice] = e[v][shifted1_slice] + return e + + def hp(h: field_t): + h[direction][boundary_slice] = h[direction][shifted1_slice] + h[u][boundary_slice] = -h[u][shifted2_slice] + h[u][shifted1_slice] = 0 + h[v][boundary_slice] = -h[v][shifted2_slice] + h[v][shifted1_slice] = 0 + return h + + return ep, hp + + else: + raise Exception('Bad polarity: {}'.format(polarity)) + + diff --git a/meanas/fdtd/energy.py b/meanas/fdtd/energy.py new file mode 100644 index 0000000..26fb036 --- /dev/null +++ b/meanas/fdtd/energy.py @@ -0,0 +1,84 @@ +from typing import List, Callable, Tuple, Dict +import numpy + +from .. import dx_lists_t, field_t, field_updater + + +def poynting(e, h): + s = (numpy.roll(e[1], -1, axis=0) * h[2] - numpy.roll(e[2], -1, axis=0) * h[1], + numpy.roll(e[2], -1, axis=1) * h[0] - numpy.roll(e[0], -1, axis=1) * h[2], + numpy.roll(e[0], -1, axis=2) * h[1] - numpy.roll(e[1], -1, axis=2) * h[0]) + return numpy.array(s) + + +def poynting_divergence(s=None, *, e=None, h=None, dxes=None): # TODO dxes + if dxes is None: + dxes = tuple(tuple(numpy.ones(1) for _ in range(3)) for _ in range(2)) + + if s is None: + s = poynting(e, h) + + ds = ((s[0] - numpy.roll(s[0], 1, axis=0)) / numpy.sqrt(dxes[0][0] * dxes[1][0])[:, None, None] + + (s[1] - numpy.roll(s[1], 1, axis=1)) / numpy.sqrt(dxes[0][1] * dxes[1][1])[None, :, None] + + (s[2] - numpy.roll(s[2], 1, axis=2)) / numpy.sqrt(dxes[0][2] * dxes[1][2])[None, None, :] ) + return ds + + +def energy_hstep(e0, h1, e2, epsilon=None, mu=None, dxes=None): + u = dxmul(e0 * e2, h1 * h1, epsilon, mu, dxes) + return u + + +def energy_estep(h0, e1, h2, epsilon=None, mu=None, dxes=None): + u = dxmul(e1 * e1, h0 * h2, epsilon, mu, dxes) + return u + + +def delta_energy_h2e(dt, e0, h1, e2, h3, epsilon=None, mu=None, dxes=None): + """ + This is just from (e2 * e2 + h3 * h1) - (h1 * h1 + e0 * e2) + """ + de = e2 * (e2 - e0) / dt + dh = h1 * (h3 - h1) / dt + du = dxmul(de, dh, epsilon, mu, dxes) + return du + + +def delta_energy_e2h(dt, h0, e1, h2, e3, epsilon=None, mu=None, dxes=None): + """ + This is just from (h2 * h2 + e3 * e1) - (e1 * e1 + h0 * h2) + """ + de = e1 * (e3 - e1) / dt + dh = h2 * (h2 - h0) / dt + du = dxmul(de, dh, epsilon, mu, dxes) + return du + + +def delta_energy_j(j0, e1, dxes=None): + if dxes is None: + dxes = tuple(tuple(numpy.ones(1) for _ in range(3)) for _ in range(2)) + + du = ((j0 * e1).sum(axis=0) * + dxes[0][0][:, None, None] * + dxes[0][1][None, :, None] * + dxes[0][2][None, None, :]) + return du + + +def dxmul(ee, hh, epsilon=None, mu=None, dxes=None): + if epsilon is None: + epsilon = 1 + if mu is None: + mu = 1 + if dxes is None: + dxes = tuple(tuple(numpy.ones(1) for _ in range(3)) for _ in range(2)) + + result = ((ee * epsilon).sum(axis=0) * + dxes[0][0][:, None, None] * + dxes[0][1][None, :, None] * + dxes[0][2][None, None, :] + + (hh * mu).sum(axis=0) * + dxes[1][0][:, None, None] * + dxes[1][1][None, :, None] * + dxes[1][2][None, None, :]) + return result diff --git a/meanas/fdtd/pml.py b/meanas/fdtd/pml.py new file mode 100644 index 0000000..3e10aa6 --- /dev/null +++ b/meanas/fdtd/pml.py @@ -0,0 +1,122 @@ +""" +PML implementations + +""" +# TODO retest pmls! + +from typing import List, Callable, Tuple, Dict +import numpy + +from .. import dx_lists_t, field_t, field_updater + + +__author__ = 'Jan Petykiewicz' + + +def cpml(direction:int, + polarity: int, + dt: float, + epsilon: field_t, + thickness: int = 8, + ln_R_per_layer: float = -1.6, + epsilon_eff: float = 1, + mu_eff: float = 1, + m: float = 3.5, + ma: float = 1, + cfs_alpha: float = 0, + dtype: numpy.dtype = numpy.float32, + ) -> Tuple[Callable, Callable, Dict[str, field_t]]: + + if direction not in range(3): + raise Exception('Invalid direction: {}'.format(direction)) + + if polarity not in (-1, 1): + raise Exception('Invalid polarity: {}'.format(polarity)) + + if thickness <= 2: + raise Exception('It would be wise to have a pml with 4+ cells of thickness') + + if epsilon_eff <= 0: + raise Exception('epsilon_eff must be positive') + + sigma_max = -ln_R_per_layer / 2 * (m + 1) + kappa_max = numpy.sqrt(epsilon_eff * mu_eff) + alpha_max = cfs_alpha + transverse = numpy.delete(range(3), direction) + u, v = transverse + + xe = numpy.arange(1, thickness+1, dtype=float) + xh = numpy.arange(1, thickness+1, dtype=float) + if polarity > 0: + xe -= 0.5 + elif polarity < 0: + xh -= 0.5 + xe = xe[::-1] + xh = xh[::-1] + else: + raise Exception('Bad polarity!') + + expand_slice = [None] * 3 + expand_slice[direction] = slice(None) + + def par(x): + scaling = (x / thickness) ** m + sigma = scaling * sigma_max + kappa = 1 + scaling * (kappa_max - 1) + alpha = ((1 - x / thickness) ** ma) * alpha_max + p0 = numpy.exp(-(sigma / kappa + alpha) * dt) + p1 = sigma / (sigma + kappa * alpha) * (p0 - 1) + p2 = 1 / kappa + return p0[expand_slice], p1[expand_slice], p2[expand_slice] + + p0e, p1e, p2e = par(xe) + p0h, p1h, p2h = par(xh) + + region = [slice(None)] * 3 + if polarity < 0: + region[direction] = slice(None, thickness) + elif polarity > 0: + region[direction] = slice(-thickness, None) + else: + raise Exception('Bad polarity!') + + se = 1 if direction == 1 else -1 + + # TODO check if epsilon is uniform in pml region? + shape = list(epsilon[0].shape) + shape[direction] = thickness + psi_e = [numpy.zeros(shape, dtype=dtype), numpy.zeros(shape, dtype=dtype)] + psi_h = [numpy.zeros(shape, dtype=dtype), numpy.zeros(shape, dtype=dtype)] + + fields = { + 'psi_e_u': psi_e[0], + 'psi_e_v': psi_e[1], + 'psi_h_u': psi_h[0], + 'psi_h_v': psi_h[1], + } + + # Note that this is kinda slow -- would be faster to reuse dHv*p2h for the original + # H update, but then you have multiple arrays and a monolithic (field + pml) update operation + def pml_e(e: field_t, h: field_t, epsilon: field_t) -> Tuple[field_t, field_t]: + dHv = h[v][region] - numpy.roll(h[v], 1, axis=direction)[region] + dHu = h[u][region] - numpy.roll(h[u], 1, axis=direction)[region] + psi_e[0] *= p0e + psi_e[0] += p1e * dHv * p2e + psi_e[1] *= p0e + psi_e[1] += p1e * dHu * p2e + e[u][region] += se * dt / epsilon[u][region] * (psi_e[0] + (p2e - 1) * dHv) + e[v][region] -= se * dt / epsilon[v][region] * (psi_e[1] + (p2e - 1) * dHu) + return e, h + + def pml_h(e: field_t, h: field_t) -> Tuple[field_t, field_t]: + dEv = (numpy.roll(e[v], -1, axis=direction)[region] - e[v][region]) + dEu = (numpy.roll(e[u], -1, axis=direction)[region] - e[u][region]) + psi_h[0] *= p0h + psi_h[0] += p1h * dEv * p2h + psi_h[1] *= p0h + psi_h[1] += p1h * dEu * p2h + h[u][region] -= se * dt * (psi_h[0] + (p2h - 1) * dEv) + h[v][region] += se * dt * (psi_h[1] + (p2h - 1) * dEu) + return e, h + + return pml_e, pml_h, fields diff --git a/fdfd_tools/test/test_fdtd.py b/meanas/test/test_fdtd.py similarity index 99% rename from fdfd_tools/test/test_fdtd.py rename to meanas/test/test_fdtd.py index d74b8be..8443afc 100644 --- a/fdfd_tools/test/test_fdtd.py +++ b/meanas/test/test_fdtd.py @@ -4,7 +4,7 @@ import dataclasses from typing import List, Tuple from numpy.testing import assert_allclose, assert_array_equal -from fdfd_tools import fdtd +from meanas import fdtd prng = numpy.random.RandomState(12345) diff --git a/meanas/types.py b/meanas/types.py new file mode 100644 index 0000000..7dc5c1c --- /dev/null +++ b/meanas/types.py @@ -0,0 +1,22 @@ +""" +Types shared across multiple submodules +""" +import numpy +from typing import List, Callable + + +# Field types +field_t = numpy.ndarray # vector field with shape (3, X, Y, Z) (e.g. [E_x, E_y, E_z]) +vfield_t = numpy.ndarray # linearized vector field (vector of length 3*X*Y*Z) + +''' + 'dxes' datastructure which contains grid cell width information in the following format: + [[[dx_e_0, dx_e_1, ...], [dy_e_0, ...], [dz_e_0, ...]], + [[dx_h_0, dx_h_1, ...], [dy_h_0, ...], [dz_h_0, ...]]] + where dx_e_0 is the x-width of the x=0 cells, as used when calculating dE/dx, + and dy_h_0 is the y-width of the y=0 cells, as used when calculating dH/dy, etc. +''' +dx_lists_t = List[List[numpy.ndarray]] + + +field_updater = Callable[[field_t], field_t] diff --git a/fdfd_tools/vectorization.py b/meanas/vectorization.py similarity index 87% rename from fdfd_tools/vectorization.py rename to meanas/vectorization.py index 57b58fb..fd6cdcf 100644 --- a/fdfd_tools/vectorization.py +++ b/meanas/vectorization.py @@ -4,15 +4,13 @@ and a 1D array representation of that field [f_x0, f_x1, f_x2,... f_y0,... f_z0, Vectorized versions of the field use row-major (ie., C-style) ordering. """ - from typing import List import numpy -__author__ = 'Jan Petykiewicz' +from .types import field_t, vfield_t -# Types -field_t = List[numpy.ndarray] # vector field (eg. [E_x, E_y, E_z] -vfield_t = numpy.ndarray # linearized vector field + +__author__ = 'Jan Petykiewicz' def vec(f: field_t) -> vfield_t: @@ -27,7 +25,7 @@ def vec(f: field_t) -> vfield_t: """ if numpy.any(numpy.equal(f, None)): return None - return numpy.hstack(tuple((fi.ravel(order='C') for fi in f))) + return numpy.ravel(f, order='C') def unvec(v: vfield_t, shape: numpy.ndarray) -> field_t: diff --git a/setup.py b/setup.py index 2a64b37..8e817cb 100644 --- a/setup.py +++ b/setup.py @@ -1,14 +1,14 @@ #!/usr/bin/env python3 from setuptools import setup, find_packages -import fdfd_tools +import meanas with open('README.md', 'r') as f: long_description = f.read() -setup(name='fdfd_tools', - version=fdfd_tools.version, - description='FDFD Electromagnetic simulation tools', +setup(name='meanas', + version=meanas.version, + description='Electromagnetic simulation tools', long_description=long_description, long_description_content_type='text/markdown', author='Jan Petykiewicz', From 94ff3f785330c6f38e9c1300de3f47a8cc876be1 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Sun, 4 Aug 2019 14:13:51 -0700 Subject: [PATCH 53/77] further fdfd_tools->meanas updates --- examples/bloch.py | 4 ++-- examples/fdfd.py | 27 +++++++++++++-------------- examples/fdtd.py | 2 +- examples/tcyl.py | 12 +++++------- meanas/fdtd/base.py | 4 ++-- 5 files changed, 23 insertions(+), 26 deletions(-) diff --git a/examples/bloch.py b/examples/bloch.py index fe1d6b1..e77c80f 100644 --- a/examples/bloch.py +++ b/examples/bloch.py @@ -1,5 +1,5 @@ -import numpy, scipy, gridlock, fdfd_tools -from fdfd_tools import bloch +import numpy, scipy, gridlock, meanas +from meanas.fdfd import bloch from numpy.linalg import norm import logging from pathlib import Path diff --git a/examples/fdfd.py b/examples/fdfd.py index a7e1746..5ee3477 100644 --- a/examples/fdfd.py +++ b/examples/fdfd.py @@ -2,11 +2,10 @@ import importlib import numpy from numpy.linalg import norm -from fdfd_tools import vec, unvec, waveguide_mode -import fdfd_tools -import fdfd_tools.functional -import fdfd_tools.grid -from fdfd_tools.solvers import generic as generic_solver +import meanas +from meanas import vec, unvec +from meanas.fdfd import waveguide_mode, functional, scpml +from meanas.fdfd.solvers import generic as generic_solver import gridlock @@ -57,8 +56,8 @@ def test0(solver=generic_solver): dxes = [grid.dxyz, grid.autoshifted_dxyz()] for a in (0, 1, 2): for p in (-1, 1): - dxes = fdfd_tools.grid.stretch_with_scpml(dxes, axis=a, polarity=p, omega=omega, - thickness=pml_thickness) + dxes = meanas.scpml.stretch_with_scpml(dxes, axis=a, polarity=p, omega=omega, + thickness=pml_thickness) J = [numpy.zeros_like(grid.grids[0], dtype=complex) for _ in range(3)] J[1][15, grid.shape[1]//2, grid.shape[2]//2] = 1e5 @@ -68,7 +67,7 @@ def test0(solver=generic_solver): ''' x = solver(J=vec(J), **sim_args) - A = fdfd_tools.functional.e_full(omega, dxes, vec(grid.grids)).tocsr() + A = functional.e_full(omega, dxes, vec(grid.grids)).tocsr() b = -1j * omega * vec(J) print('Norm of the residual is ', norm(A @ x - b)) @@ -113,8 +112,8 @@ def test1(solver=generic_solver): dxes = [grid.dxyz, grid.autoshifted_dxyz()] for a in (0, 1, 2): for p in (-1, 1): - dxes = fdfd_tools.grid.stretch_with_scpml(dxes,omega=omega, axis=a, polarity=p, - thickness=pml_thickness) + dxes = scpml.stretch_with_scpml(dxes,omega=omega, axis=a, polarity=p, + thickness=pml_thickness) half_dims = numpy.array([10, 20, 15]) * dx dims = [-half_dims, half_dims] @@ -155,7 +154,7 @@ def test1(solver=generic_solver): x = solver(J=vec(J), **sim_args) b = -1j * omega * vec(J) - A = fdfd_tools.operators.e_full(**sim_args).tocsr() + A = operators.e_full(**sim_args).tocsr() print('Norm of the residual is ', norm(A @ x - b)) E = unvec(x, grid.shape) @@ -181,9 +180,9 @@ def test1(solver=generic_solver): def poyntings(E): e = vec(E) - h = fdfd_tools.operators.e2h(omega, dxes) @ e - cross1 = fdfd_tools.operators.poynting_e_cross(e, dxes) @ h.conj() - cross2 = fdfd_tools.operators.poynting_h_cross(h.conj(), dxes) @ e + h = operators.e2h(omega, dxes) @ e + cross1 = operators.poynting_e_cross(e, dxes) @ h.conj() + cross2 = operators.poynting_h_cross(h.conj(), dxes) @ e s1 = unvec(0.5 * numpy.real(cross1), grid.shape) s2 = unvec(0.5 * numpy.real(-cross2), grid.shape) return s1, s2 diff --git a/examples/fdtd.py b/examples/fdtd.py index 1a25be4..be3942b 100644 --- a/examples/fdtd.py +++ b/examples/fdtd.py @@ -10,7 +10,7 @@ import time import numpy import h5py -from fdfd_tools import fdtd +from meanas import fdtd from masque import Pattern, shapes import gridlock import pcgen diff --git a/examples/tcyl.py b/examples/tcyl.py index 66caeb2..b0b57a2 100644 --- a/examples/tcyl.py +++ b/examples/tcyl.py @@ -2,11 +2,9 @@ import importlib import numpy from numpy.linalg import norm -from fdfd_tools import vec, unvec, waveguide_mode -import fdfd_tools -import fdfd_tools.functional -import fdfd_tools.grid -from fdfd_tools.solvers import generic as generic_solver +from meanas import vec, unvec +from meanas.fdfd import waveguide_mode, functional, scpml +from meanas.fdfd.solvers import generic as generic_solver import gridlock @@ -50,8 +48,8 @@ def test1(solver=generic_solver): dxes = [grid.dxyz, grid.autoshifted_dxyz()] for a in (1, 2): for p in (-1, 1): - dxes = fdfd_tools.grid.stretch_with_scpml(dxes, omega=omega, axis=a, polarity=p, - thickness=pml_thickness) + dxes = scmpl.stretch_with_scpml(dxes, omega=omega, axis=a, polarity=p, + thickness=pml_thickness) wg_args = { 'omega': omega, diff --git a/meanas/fdtd/base.py b/meanas/fdtd/base.py index 8dd1df3..389a7b5 100644 --- a/meanas/fdtd/base.py +++ b/meanas/fdtd/base.py @@ -13,7 +13,7 @@ def curl_h(dxes: dx_lists_t = None) -> field_updater: """ Curl operator for use with the H field. - :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header + :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types :return: Function for taking the discretized curl of the H-field, F(H) -> curlH """ if dxes: @@ -42,7 +42,7 @@ def curl_e(dxes: dx_lists_t = None) -> field_updater: """ Curl operator for use with the E field. - :param dxes: Grid parameters [dx_e, dx_h] as described in fdfd_tools.operators header + :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types :return: Function for taking the discretized curl of the E-field, F(E) -> curlE """ if dxes is not None: From 5951f2bdb12bb57bc5ddaaa0b28de5d1f96acf34 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 5 Aug 2019 00:20:06 -0700 Subject: [PATCH 54/77] various fixes and improvements --- examples/fdfd.py | 17 +++++++++----- meanas/fdfd/farfield.py | 10 ++++---- meanas/fdfd/functional.py | 2 +- meanas/fdfd/operators.py | 2 +- meanas/fdfd/scpml.py | 2 ++ meanas/fdfd/waveguide.py | 17 +++++++------- meanas/fdfd/waveguide_mode.py | 43 ++++++++--------------------------- meanas/test/test_fdtd.py | 1 + 8 files changed, 40 insertions(+), 54 deletions(-) diff --git a/examples/fdfd.py b/examples/fdfd.py index 5ee3477..c20fc3e 100644 --- a/examples/fdfd.py +++ b/examples/fdfd.py @@ -4,7 +4,7 @@ from numpy.linalg import norm import meanas from meanas import vec, unvec -from meanas.fdfd import waveguide_mode, functional, scpml +from meanas.fdfd import waveguide_mode, functional, scpml, operators from meanas.fdfd.solvers import generic as generic_solver import gridlock @@ -56,18 +56,23 @@ def test0(solver=generic_solver): dxes = [grid.dxyz, grid.autoshifted_dxyz()] for a in (0, 1, 2): for p in (-1, 1): - dxes = meanas.scpml.stretch_with_scpml(dxes, axis=a, polarity=p, omega=omega, - thickness=pml_thickness) + dxes = meanas.fdfd.scpml.stretch_with_scpml(dxes, axis=a, polarity=p, omega=omega, + thickness=pml_thickness) J = [numpy.zeros_like(grid.grids[0], dtype=complex) for _ in range(3)] - J[1][15, grid.shape[1]//2, grid.shape[2]//2] = 1e5 + J[1][15, grid.shape[1]//2, grid.shape[2]//2] = 1 ''' Solve! ''' + sim_args = { + 'omega': omega, + 'dxes': dxes, + 'epsilon': vec(grid.grids), + } x = solver(J=vec(J), **sim_args) - A = functional.e_full(omega, dxes, vec(grid.grids)).tocsr() + A = operators.e_full(omega, dxes, vec(grid.grids)).tocsr() b = -1j * omega * vec(J) print('Norm of the residual is ', norm(A @ x - b)) @@ -208,7 +213,7 @@ def module_available(name): if __name__ == '__main__': - # test0() + test0() if module_available('opencl_fdfd'): from opencl_fdfd import cg_solver as opencl_solver diff --git a/meanas/fdfd/farfield.py b/meanas/fdfd/farfield.py index 84a04ba..faa25b5 100644 --- a/meanas/fdfd/farfield.py +++ b/meanas/fdfd/farfield.py @@ -6,9 +6,11 @@ import numpy from numpy.fft import fft2, fftshift, fftfreq, ifft2, ifftshift from numpy import pi +from .. import field_t -def near_to_farfield(E_near: List[numpy.ndarray], - H_near: List[numpy.ndarray], + +def near_to_farfield(E_near: field_t, + H_near: field_t, dx: float, dy: float, padded_size: List[int] = None @@ -115,8 +117,8 @@ def near_to_farfield(E_near: List[numpy.ndarray], -def far_to_nearfield(E_far: List[numpy.ndarray], - H_far: List[numpy.ndarray], +def far_to_nearfield(E_far: field_t, + H_far: field_t, dkx: float, dky: float, padded_size: List[int] = None diff --git a/meanas/fdfd/functional.py b/meanas/fdfd/functional.py index e57fe88..dd94421 100644 --- a/meanas/fdfd/functional.py +++ b/meanas/fdfd/functional.py @@ -8,7 +8,7 @@ e.g. E = [E_x, E_y, E_z] where each component has shape (X, Y, Z) from typing import List, Callable import numpy -from . import dx_lists_t, field_t +from .. import dx_lists_t, field_t __author__ = 'Jan Petykiewicz' diff --git a/meanas/fdfd/operators.py b/meanas/fdfd/operators.py index 3b2de68..774c3d9 100644 --- a/meanas/fdfd/operators.py +++ b/meanas/fdfd/operators.py @@ -32,7 +32,7 @@ from typing import List, Tuple import numpy import scipy.sparse as sparse -from . import vec, dx_lists_t, vfield_t +from .. import vec, dx_lists_t, vfield_t __author__ = 'Jan Petykiewicz' diff --git a/meanas/fdfd/scpml.py b/meanas/fdfd/scpml.py index c4091a0..897d43a 100644 --- a/meanas/fdfd/scpml.py +++ b/meanas/fdfd/scpml.py @@ -5,6 +5,8 @@ Functions for creating stretched coordinate PMLs. from typing import List, Callable import numpy +from .. import dx_lists_t + __author__ = 'Jan Petykiewicz' diff --git a/meanas/fdfd/waveguide.py b/meanas/fdfd/waveguide.py index 48a1510..91b023c 100644 --- a/meanas/fdfd/waveguide.py +++ b/meanas/fdfd/waveguide.py @@ -23,7 +23,7 @@ import numpy from numpy.linalg import norm import scipy.sparse as sparse -from . import vec, unvec, dx_lists_t, field_t, vfield_t +from .. import vec, unvec, dx_lists_t, field_t, vfield_t from . import operators @@ -82,7 +82,8 @@ def normalized_fields(v: numpy.ndarray, omega: complex, dxes: dx_lists_t, epsilon: vfield_t, - mu: vfield_t = None + mu: vfield_t = None, + dx_prop: float = 0, ) -> Tuple[vfield_t, vfield_t]: """ Given a vector v containing the vectorized H_x and H_y fields, @@ -94,6 +95,7 @@ def normalized_fields(v: numpy.ndarray, :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types (2D) :param epsilon: Vectorized dielectric constant grid :param mu: Vectorized magnetic permeability grid (default 1 everywhere) + :param dxes_prop: Grid cell width in the propagation direction. Default 0 (continuous). :return: Normalized, vectorized (e, h) containing all vector components. """ e = v2e(v, wavenumber, omega, dxes, epsilon, mu=mu) @@ -105,11 +107,10 @@ def normalized_fields(v: numpy.ndarray, E = unvec(e, shape) H = unvec(h, shape) - S1 = E[0] * numpy.roll(numpy.conj(H[1]), 1, axis=0) * dxes_real[0][1] * dxes_real[1][0] - S2 = E[1] * numpy.roll(numpy.conj(H[0]), 1, axis=1) * dxes_real[0][0] * dxes_real[1][1] - S = 0.25 * ((S1 + numpy.roll(S1, 1, axis=0)) - - (S2 + numpy.roll(S2, 1, axis=1))) - P = 0.5 * numpy.real(S.sum()) + phase = numpy.exp(-1j * wavenumber * dx_prop / 2) + S1 = E[0] * numpy.conj(H[1] * phase) * dxes_real[0][1] * dxes_real[1][0] + S2 = E[1] * numpy.conj(H[0] * phase) * dxes_real[0][0] * dxes_real[1][1] + P = numpy.real(S1.sum() - S2.sum()) assert P > 0, 'Found a mode propagating in the wrong direction! P={}'.format(P) energy = epsilon * e.conj() * e @@ -120,8 +121,6 @@ def normalized_fields(v: numpy.ndarray, # Try to break symmetry to assign a consistent sign [experimental] E_weighted = unvec(e * energy * numpy.exp(1j * norm_angle), shape) sign = numpy.sign(E_weighted[:, :max(shape[0]//2, 1), :max(shape[1]//2, 1)].real.sum()) - logger.debug('norm_angle = {}'.format(norm_angle)) - logger.debug('norm_sign = {}'.format(sign) norm_factor = sign * norm_amplitude * numpy.exp(1j * norm_angle) diff --git a/meanas/fdfd/waveguide_mode.py b/meanas/fdfd/waveguide_mode.py index 6e2b192..7182377 100644 --- a/meanas/fdfd/waveguide_mode.py +++ b/meanas/fdfd/waveguide_mode.py @@ -2,9 +2,9 @@ from typing import Dict, List import numpy import scipy.sparse as sparse -from . import vec, unvec, dx_lists_t, vfield_t, field_t +from .. import vec, unvec, dx_lists_t, vfield_t, field_t from . import operators, waveguide, functional -from .eigensolvers import signed_eigensolve, rayleigh_quotient_iteration +from ..eigensolvers import signed_eigensolve, rayleigh_quotient_iteration def solve_waveguide_mode_2d(mode_number: int, @@ -12,7 +12,7 @@ def solve_waveguide_mode_2d(mode_number: int, dxes: dx_lists_t, epsilon: vfield_t, mu: vfield_t = None, - wavenumber_correction: bool = True, + dx_prop: float = 0, ) -> Dict[str, complex or field_t]: """ Given a 2d region, attempts to solve for the eigenmode with the specified mode number. @@ -22,8 +22,8 @@ def solve_waveguide_mode_2d(mode_number: int, :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types :param epsilon: Dielectric constant :param mu: Magnetic permeability (default 1 everywhere) - :param wavenumber_correction: Whether to correct the wavenumber to - account for numerical dispersion (default True) + :param dx_prop: The cell width in the the propagation direction, used to apply a + correction to the wavenumber. Default 0 (i.e. continuous propagation direction) :return: {'E': List[numpy.ndarray], 'H': List[numpy.ndarray], 'wavenumber': complex} """ @@ -51,15 +51,9 @@ def solve_waveguide_mode_2d(mode_number: int, ''' Perform correction on wavenumber to account for numerical dispersion. - - See Numerical Dispersion in Taflove's FDTD book. - This correction term reduces the error in emitted power, but additional - error is introduced into the E_err and H_err terms. This effect becomes - more pronounced as the wavenumber increases. ''' - if wavenumber_correction: - dx_mean = (numpy.hstack(dxes[0]) + numpy.hstack(dxes[1])).mean() / 2 #TODO figure out what dx to use here - wavenumber -= 2 * numpy.sin(numpy.real(wavenumber * dx_mean / 2)) / dx_mean - numpy.real(wavenumber) + if dx_prop != 0: + wavenumber = 2 / dx_prop * numpy.sin(wavenumber * dx_prop / 2) shape = [d.size for d in dxes[0]] fields = { @@ -79,7 +73,6 @@ def solve_waveguide_mode(mode_number: int, slices: List[slice], epsilon: field_t, mu: field_t = None, - wavenumber_correction: bool = True ) -> Dict[str, complex or numpy.ndarray]: """ Given a 3D grid, selects a slice from the grid and attempts to @@ -94,8 +87,6 @@ def solve_waveguide_mode(mode_number: int, as the waveguide cross-section. slices[axis] should select only one :param epsilon: Dielectric constant :param mu: Magnetic permeability (default 1 everywhere) - :param wavenumber_correction: Whether to correct the wavenumber to - account for numerical dispersion (default True) :return: {'E': List[numpy.ndarray], 'H': List[numpy.ndarray], 'wavenumber': complex} """ if mu is None: @@ -115,7 +106,7 @@ def solve_waveguide_mode(mode_number: int, 'dxes': [[dx[i][slices[i]] for i in order[:2]] for dx in dxes], 'epsilon': vec([epsilon[i][slices].transpose(order) for i in order]), 'mu': vec([mu[i][slices].transpose(order) for i in order]), - 'wavenumber_correction': wavenumber_correction, + 'dx_prop': dxes[0][order[2]][slices[order[2]]], } fields_2d = solve_waveguide_mode_2d(mode_number, omega=omega, **args_2d) @@ -175,9 +166,6 @@ def compute_source(E: field_t, :param mu: Magnetic permeability (default 1 everywhere) :return: J distribution for the unidirectional source """ - if mu is None: - mu = numpy.ones(3) - J = numpy.zeros_like(E, dtype=complex) M = numpy.zeros_like(E, dtype=complex) @@ -275,9 +263,9 @@ def solve_waveguide_mode_cylindrical(mode_number: int, dxes: dx_lists_t, epsilon: vfield_t, r0: float, - wavenumber_correction: bool = True, ) -> Dict[str, complex or field_t]: """ + TODO: fixup Given a 2d (r, y) slice of epsilon, attempts to solve for the eigenmode of the bent waveguide with the specified mode number. @@ -288,8 +276,6 @@ def solve_waveguide_mode_cylindrical(mode_number: int, :param epsilon: Dielectric constant :param r0: Radius of curvature for the simulation. This should be the minimum value of r within the simulation domain. - :param wavenumber_correction: Whether to correct the wavenumber to - account for numerical dispersion (default True) :return: {'E': List[numpy.ndarray], 'H': List[numpy.ndarray], 'wavenumber': complex} """ @@ -313,16 +299,7 @@ def solve_waveguide_mode_cylindrical(mode_number: int, wavenumber = numpy.sqrt(eigval) wavenumber *= numpy.sign(numpy.real(wavenumber)) - ''' - Perform correction on wavenumber to account for numerical dispersion. - - See Numerical Dispersion in Taflove's FDTD book. - This correction term reduces the error in emitted power, but additional - error is introduced into the E_err and H_err terms. This effect becomes - more pronounced as the wavenumber increases. - ''' - if wavenumber_correction: - wavenumber -= 2 * numpy.sin(numpy.real(wavenumber / 2)) - numpy.real(wavenumber) + # TODO: Perform correction on wavenumber to account for numerical dispersion. shape = [d.size for d in dxes[0]] v = numpy.hstack((v, numpy.zeros(shape[0] * shape[1]))) diff --git a/meanas/test/test_fdtd.py b/meanas/test/test_fdtd.py index 8443afc..08b678e 100644 --- a/meanas/test/test_fdtd.py +++ b/meanas/test/test_fdtd.py @@ -9,6 +9,7 @@ from meanas import fdtd prng = numpy.random.RandomState(12345) + def assert_fields_close(a, b, *args, **kwargs): numpy.testing.assert_allclose(a, b, verbose=False, err_msg='Fields did not match:\n{}\n{}'.format(numpy.rollaxis(a, -1), numpy.rollaxis(b, -1)), *args, **kwargs) From 938c4c9a3503efc429eabeefbebff7a50630506c Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 5 Aug 2019 01:09:52 -0700 Subject: [PATCH 55/77] move to 3xnnn arrays --- meanas/fdfd/functional.py | 43 +++++++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/meanas/fdfd/functional.py b/meanas/fdfd/functional.py index dd94421..655d9b8 100644 --- a/meanas/fdfd/functional.py +++ b/meanas/fdfd/functional.py @@ -29,9 +29,13 @@ def curl_h(dxes: dx_lists_t) -> functional_matrix: return (f - numpy.roll(f, 1, axis=ax)) / dxyz_b[ax] def ch_fun(h: field_t) -> field_t: - e = [dh(h[2], 1) - dh(h[1], 2), - dh(h[0], 2) - dh(h[2], 0), - dh(h[1], 0) - dh(h[0], 1)] + e = numpy.empty_like(h) + e[0] = dh(h[2], 1) + e[0] -= dh(h[1], 2) + e[1] = dh(h[0], 2) + e[1] -= dh(h[2], 0) + e[2] = dh(h[1], 0) + e[2] -= dh(h[0], 1) return e return ch_fun @@ -50,9 +54,13 @@ def curl_e(dxes: dx_lists_t) -> functional_matrix: return (numpy.roll(f, -1, axis=ax) - f) / dxyz_a[ax] def ce_fun(e: field_t) -> field_t: - h = [de(e[2], 1) - de(e[1], 2), - de(e[0], 2) - de(e[2], 0), - de(e[1], 0) - de(e[0], 1)] + h = numpy.empty_like(e) + h[0] = de(e[2], 1) + h[0] -= de(e[1], 2) + h[1] = de(e[0], 2) + h[1] -= de(e[2], 0) + h[2] = de(e[1], 0) + h[2] -= de(e[0], 1) return h return ce_fun @@ -79,11 +87,11 @@ def e_full(omega: complex, def op_1(e): curls = ch(ce(e)) - return [c - omega ** 2 * e * x for c, e, x in zip(curls, epsilon, e)] + return curls - omega ** 2 * epsilon * e def op_mu(e): - curls = ch([m * y for m, y in zip(mu, ce(e))]) - return [c - omega ** 2 * p * x for c, p, x in zip(curls, epsilon, e)] + curls = ch(mu * ce(e)) + return curls - omega ** 2 * epsilon * e if numpy.any(numpy.equal(mu, None)): return op_1 @@ -109,12 +117,12 @@ def eh_full(omega: complex, ce = curl_e(dxes) def op_1(e, h): - return ([c - 1j * omega * p * x for c, p, x in zip(ch(h), epsilon, e)], - [c + 1j * omega * y for c, y in zip(ce(e), h)]) + return (ch(h) - 1j * omega * epsilon * e, + ce(e) + 1j * omega * h) def op_mu(e, h): - return ([c - 1j * omega * p * x for c, p, x in zip(ch(h), epsilon, e)], - [c + 1j * omega * m * y for c, m, y in zip(ce(e), mu, h)]) + return (ch(h) - 1j * omega * epsilon * e, + ce(e) + 1j * omega * mu * h) if numpy.any(numpy.equal(mu, None)): return op_1 @@ -138,10 +146,10 @@ def e2h(omega: complex, A2 = curl_e(dxes) def e2h_1_1(e): - return [y / (-1j * omega) for y in A2(e)] + return A2(e) / (-1j * omega) def e2h_mu(e): - return [y / (-1j * omega * m) for y, m in zip(A2(e), mu)] + return A2(e) / (-1j * omega * mu) if numpy.any(numpy.equal(mu, None)): return e2h_1_1 @@ -166,12 +174,11 @@ def m2j(omega: complex, ch = curl_h(dxes) def m2j_mu(m): - m_mu = [m[k] / mu[k] for k in range[3]] - J = [Ji / (-1j * omega) for Ji in ch(m_mu)] + J = ch(m / mu) / (-1j * omega) return J def m2j_1(m): - J = [Ji / (-1j * omega) for Ji in ch(m)] + J = ch(m) / (-1j * omega) return J if numpy.any(numpy.equal(mu, None)): From 342912099344b19d7fb78f7048cb686b935c3024 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Wed, 7 Aug 2019 01:00:21 -0700 Subject: [PATCH 56/77] d_prop -> dx_prop --- meanas/fdfd/waveguide_mode.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/meanas/fdfd/waveguide_mode.py b/meanas/fdfd/waveguide_mode.py index 7182377..7fab6e6 100644 --- a/meanas/fdfd/waveguide_mode.py +++ b/meanas/fdfd/waveguide_mode.py @@ -101,28 +101,28 @@ def solve_waveguide_mode(mode_number: int, order = numpy.roll(range(3), 2 - axis) reverse_order = numpy.roll(range(3), axis - 2) + # Find dx in propagation direction + dxab_forward = numpy.array([dx[order[2]][slices[order[2]]] for dx in dxes]) + dx_prop = 0.5 * sum(dxab_forward) + # Reduce to 2D and solve the 2D problem args_2d = { 'dxes': [[dx[i][slices[i]] for i in order[:2]] for dx in dxes], 'epsilon': vec([epsilon[i][slices].transpose(order) for i in order]), 'mu': vec([mu[i][slices].transpose(order) for i in order]), - 'dx_prop': dxes[0][order[2]][slices[order[2]]], + 'dx_prop': dx_prop, } fields_2d = solve_waveguide_mode_2d(mode_number, omega=omega, **args_2d) ''' Apply corrections and expand to 3D ''' - # Scale based on dx in propagation direction - dxab_forward = numpy.array([dx[order[2]][slices[order[2]]] for dx in dxes]) - # Adjust for propagation direction fields_2d['E'][2] *= polarity fields_2d['H'][2] *= polarity # Apply phase shift to H-field - d_prop = 0.5 * sum(dxab_forward) - fields_2d['H'] *= numpy.exp(-polarity * 1j * 0.5 * fields_2d['wavenumber'] * d_prop) + fields_2d['H'] *= numpy.exp(-polarity * 1j * 0.5 * fields_2d['wavenumber'] * dx_prop) # Expand E, H to full epsilon space we were given E = numpy.zeros_like(epsilon, dtype=complex) @@ -136,7 +136,6 @@ def solve_waveguide_mode(mode_number: int, 'H': H, 'E': E, } - return results From 2c91ea249f87dc9572480f26dc37f563965e85b7 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Wed, 7 Aug 2019 01:00:57 -0700 Subject: [PATCH 57/77] Fix wgmode expansion --- meanas/fdfd/waveguide_mode.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/meanas/fdfd/waveguide_mode.py b/meanas/fdfd/waveguide_mode.py index 7fab6e6..b6a66de 100644 --- a/meanas/fdfd/waveguide_mode.py +++ b/meanas/fdfd/waveguide_mode.py @@ -436,16 +436,15 @@ def expand_wgmode_e(E: field_t, phase_E = numpy.exp(iphi * r_E).reshape(a_shape) # Expand our slice to the entire grid using the phase factors - Ee = numpy.zeros_like(E) + E_expanded = numpy.zeros_like(E) slices_exp = list(slices) slices_exp[axis] = slice(E.shape[axis + 1]) slices_exp = (slice(None), *slices_exp) - slices_in = tuple(slice(None), *slices) + slices_in = (slice(None), *slices) - Ee[slices_exp] = phase_E * numpy.array(E)[slices_in] - - return Ee + E_expanded[slices_exp] = phase_E * numpy.array(E)[slices_in] + return E_expanded From 1a04bab361b4db0933a6215310990aa1c9daf011 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Wed, 7 Aug 2019 01:01:35 -0700 Subject: [PATCH 58/77] Fixup slices --- meanas/fdfd/waveguide_mode.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/meanas/fdfd/waveguide_mode.py b/meanas/fdfd/waveguide_mode.py index b6a66de..1603547 100644 --- a/meanas/fdfd/waveguide_mode.py +++ b/meanas/fdfd/waveguide_mode.py @@ -346,16 +346,26 @@ def compute_source_e(QE: field_t, mu: field_t = None, ) -> field_t: """ - Want (AQ-QA) E = -iwJ, where Q is a mask - If E is an eigenmode, AE = 0 so just AQE = -iwJ - Really only need E in 4 cells along axis (0, 0, Emode1, Emode2), find AE (1 fdtd step), then use center 2 cells as src + Want AQE = -iwJ, where Q is mask and normally AE = -iwJ + ## Want (AQ-QA) E = -iwJ, where Q is a mask + ## If E is an eigenmode, AE = 0 so just AQE = -iwJ + Really only need E in 4 cells along axis (0, 0, Emode1, Emode2), find AE (1 iteration), then use center 2 cells as src + Maybe better to use (0, Emode1, Emode2, Emode3), find AE (1 iteration), then use left 2 cells as src? """ slices = tuple(slices) # Trim a cell from each end of the propagation axis slices_reduced = list(slices) - slices_reduced[axis] = slice(slices[axis].start + 1, slices[axis].stop - 1) - slices_reduced = tuple(slice(None), *slices_reduced) + for aa in range(3): + if aa == axis: + if polarity > 0: + slices_reduced[axis] = slice(slices[axis].start, slices[axis].start+2) + else: + slices_reduced[axis] = slice(slices[axis].stop-2, slices[axis].stop) + else: + start = slices[aa].start + stop = slices[aa].stop + slices_reduced = (slice(None), *slices_reduced) # Don't actually need to mask out E here since it needs to be pre-masked (QE) @@ -410,7 +420,7 @@ def compute_overlap_ce(E: field_t, slices2 = list(slices) slices2[axis] = slice(start, stop) - slices2 = tuple(slice(None), slices2) + slices2 = (slice(None), *slices2) Etgt = numpy.zeros_like(Ee) Etgt[slices2] = Ee[slices2] From 07c94617fefd5b5da72331958d1e48350601ba0d Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Wed, 7 Aug 2019 01:01:55 -0700 Subject: [PATCH 59/77] Operator-based soruce --- meanas/fdfd/waveguide_mode.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/meanas/fdfd/waveguide_mode.py b/meanas/fdfd/waveguide_mode.py index 1603547..1cce856 100644 --- a/meanas/fdfd/waveguide_mode.py +++ b/meanas/fdfd/waveguide_mode.py @@ -170,18 +170,25 @@ def compute_source(E: field_t, src_order = numpy.roll(range(3), -axis) exp_iphi = numpy.exp(1j * polarity * wavenumber * dxes[1][axis][slices[axis]]) - J[src_order[1]] = +exp_iphi * H[src_order[2]] * polarity - J[src_order[2]] = -exp_iphi * H[src_order[1]] * polarity rollby = -1 if polarity > 0 else 0 - M[src_order[1]] = +numpy.roll(E[src_order[2]], rollby, axis=axis) - M[src_order[2]] = -numpy.roll(E[src_order[1]], rollby, axis=axis) +# J[src_order[1]] = +exp_iphi * H[src_order[2]] * polarity / dxes[1][axis][slices[axis]] +# J[src_order[2]] = -exp_iphi * H[src_order[1]] * polarity / dxes[1][axis][slices[axis]] +# M[src_order[1]] = +numpy.roll(E[src_order[2]], rollby, axis=axis) / dxes[0][axis][slices[axis]] +# M[src_order[2]] = -numpy.roll(E[src_order[1]], rollby, axis=axis) / dxes[0][axis][slices[axis]] + + s2 = [slice(None), slice(None), slice(None)] + s2[axis] = slice(slices[axis].start, slices[axis].stop) + s2 = (src_order, *s2) + + J[s2] = numpy.roll(functional.curl_h(dxes=dxes)(H), rollby, axis=axis+1)[s2] * polarity * exp_iphi + M[s2] = numpy.roll(functional.curl_e(dxes=dxes)(E), -rollby, axis=axis+1)[s2] m2j = functional.m2j(omega, dxes, mu) Jm = m2j(M) Jtot = J + Jm - return Jtot + return Jtot.conj() def compute_overlap_e(E: field_t, @@ -332,7 +339,6 @@ def compute_source_q(E: field_t, Jm = m2j(M) Jtot = J + Jm - return Jtot, J, M From aade81c21e9df18643d18404fffcf10980d1dfc4 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Wed, 7 Aug 2019 02:27:04 -0700 Subject: [PATCH 60/77] alternate src formulation --- meanas/fdfd/waveguide_mode.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/meanas/fdfd/waveguide_mode.py b/meanas/fdfd/waveguide_mode.py index 1cce856..59a3f37 100644 --- a/meanas/fdfd/waveguide_mode.py +++ b/meanas/fdfd/waveguide_mode.py @@ -169,9 +169,9 @@ def compute_source(E: field_t, M = numpy.zeros_like(E, dtype=complex) src_order = numpy.roll(range(3), -axis) - exp_iphi = numpy.exp(1j * polarity * wavenumber * dxes[1][axis][slices[axis]]) - rollby = -1 if polarity > 0 else 0 +# exp_iphi = numpy.exp(1j * polarity * wavenumber * dxes[1][axis][slices[axis]]) +# rollby = -1 if polarity > 0 else 0 # J[src_order[1]] = +exp_iphi * H[src_order[2]] * polarity / dxes[1][axis][slices[axis]] # J[src_order[2]] = -exp_iphi * H[src_order[1]] * polarity / dxes[1][axis][slices[axis]] # M[src_order[1]] = +numpy.roll(E[src_order[2]], rollby, axis=axis) / dxes[0][axis][slices[axis]] @@ -181,14 +181,16 @@ def compute_source(E: field_t, s2[axis] = slice(slices[axis].start, slices[axis].stop) s2 = (src_order, *s2) - J[s2] = numpy.roll(functional.curl_h(dxes=dxes)(H), rollby, axis=axis+1)[s2] * polarity * exp_iphi - M[s2] = numpy.roll(functional.curl_e(dxes=dxes)(E), -rollby, axis=axis+1)[s2] + rollby = 1 if polarity > 0 else 0 + exp_iphi = numpy.exp(-1j * polarity * wavenumber * dxes[1][axis][slices[axis]]) + J[s2] = numpy.roll(functional.curl_h(dxes=dxes)(H.conj()), -rollby, axis=axis+1)[s2] * polarity * exp_iphi + M[s2] = -numpy.roll(functional.curl_e(dxes=dxes)(E.conj()), rollby, axis=axis+1)[s2] m2j = functional.m2j(omega, dxes, mu) Jm = m2j(M) Jtot = J + Jm - return Jtot.conj() + return Jtot def compute_overlap_e(E: field_t, @@ -371,6 +373,17 @@ def compute_source_e(QE: field_t, else: start = slices[aa].start stop = slices[aa].stop +# if start is not None or stop is not None: +# if start is None: +# start = 1 +# stop -= 1 +# elif stop is None: +# stop = E.shape[aa + 1] - 1 +# start += 1 +# else: +# start += 1 +# stop -= 1 +# slices_reduced[aa] = slice(start, stop) slices_reduced = (slice(None), *slices_reduced) # Don't actually need to mask out E here since it needs to be pre-masked (QE) From ccdb423ba2091ff911fcecd48494cb0e123122cf Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 26 Aug 2019 00:15:34 -0700 Subject: [PATCH 61/77] add e_tfsf_source --- meanas/fdfd/functional.py | 17 +++++++++++++++++ meanas/fdfd/operators.py | 18 ++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/meanas/fdfd/functional.py b/meanas/fdfd/functional.py index 655d9b8..fe11e51 100644 --- a/meanas/fdfd/functional.py +++ b/meanas/fdfd/functional.py @@ -187,3 +187,20 @@ def m2j(omega: complex, return m2j_mu +def e_tfsf_source(TF_region: field_t, + omega: complex, + dxes: dx_lists_t, + epsilon: field_t, + mu: field_t = None, + ) -> functional_matrix: + """ + Operator that turuns an E-field distribution into a total-field/scattered-field + (TFSF) source. + """ + # TODO documentation + A = e_full(omega, dxes, epsilon, mu) + + def op(e): + neg_iwj = A(TF_region * e) - TF_region * A(e) + return neg_iwj / (-1j * omega) + diff --git a/meanas/fdfd/operators.py b/meanas/fdfd/operators.py index 774c3d9..3042af4 100644 --- a/meanas/fdfd/operators.py +++ b/meanas/fdfd/operators.py @@ -501,3 +501,21 @@ def poynting_h_cross(h: vfield_t, dxes: dx_lists_t) -> sparse.spmatrix: [ bx @ Hz @ fy @ dagx, zero, -bz @ Hx @ fy @ dagz], [-bx @ Hy @ fz @ dagx, by @ Hx @ fz @ dagy, zero]]) return P + + +def e_tfsf_source(TF_region: vfield_t, + omega: complex, + dxes: dx_lists_t, + epsilon: vfield_t, + mu: vfield_t = None, + ) -> sparse.spmatrix: + """ + Operator that turns an E-field distribution into a total-field/scattered-field + (TFSF) source. + """ + # TODO documentation + A = e_full(omega, dxes, epsilon, mu) + Q = sparse.diags(TF_region) + return (A @ Q - Q @ A) / (-1j * omega) + + From b466ed02eae2d4420ef7318683c754ed1b358341 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 26 Aug 2019 00:16:27 -0700 Subject: [PATCH 62/77] Add e_boundary_source --- meanas/fdfd/operators.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/meanas/fdfd/operators.py b/meanas/fdfd/operators.py index 3042af4..e156d2e 100644 --- a/meanas/fdfd/operators.py +++ b/meanas/fdfd/operators.py @@ -519,3 +519,32 @@ def e_tfsf_source(TF_region: vfield_t, return (A @ Q - Q @ A) / (-1j * omega) +def e_boundary_source(mask: vfield_t, + omega: complex, + dxes: dx_lists_t, + epsilon: vfield_t, + mu: vfield_t = None, + periodic_mask_edges: bool = False, + ) -> sparse.spmatrix: + """ + Operator that turns an E-field distrubtion into a current (J) distribution + along the edges (external and internal) of the provided mask. This is just an + e_tfsf_source with an additional masking step. + """ + full = e_tfsf_source(TF_region=mask, omega=omega, dxes=dxes, epsilon=epsilon, mu=mu) + + shape = [len(dxe) for dxe in dxes[0]] + jmask = numpy.zeros_like(mask, dtype=bool) + + if periodic_mask_edges: + shift = lambda axis, polarity: rotation(axis=axis, shape=shape, shift_distance=polarity) + else: + shift = lambda axis, polarity: shift_with_mirror(axis=axis, shape=shape, shift_distance=polarity) + + for axis in (0, 1, 2): + for polarity in (-1, +1): + r = shift(axis, polarity) - sparse.eye(numpy.prod(shape)) # shifted minus original + r3 = sparse.block_diag((r, r, r)) + jmask = numpy.logical_or(jmask, numpy.abs(r3 @ mask)) + + return sparse.diags(jmask.astype(int)) @ full, jmask From 0503e9d6ef88f43c9840897037a145f7a2b8f3f1 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 26 Aug 2019 00:16:45 -0700 Subject: [PATCH 63/77] Fix shift_with_mirror() for C-ordered arrays --- meanas/fdfd/operators.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/meanas/fdfd/operators.py b/meanas/fdfd/operators.py index e156d2e..13b2691 100644 --- a/meanas/fdfd/operators.py +++ b/meanas/fdfd/operators.py @@ -341,9 +341,7 @@ def shift_with_mirror(axis: int, shape: List[int], shift_distance: int=1) -> spa n = numpy.prod(shape) i_ind = numpy.arange(n) - j_ind = ijk[0] + ijk[1] * shape[0] - if len(shape) == 3: - j_ind += ijk[2] * shape[0] * shape[1] + j_ind = numpy.ravel_multi_index(ijk, shape, order='C') vij = (numpy.ones(n), (i_ind, j_ind.ravel(order='C'))) From 054ac994d501c214af6f7d8e90c86b8ab24a5a6d Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 26 Aug 2019 00:17:52 -0700 Subject: [PATCH 64/77] Don't perform dx_prop wavenumber correction in waveguide_mode_2d It's technically a correction for discretization in the third direction --- meanas/fdfd/waveguide_mode.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/meanas/fdfd/waveguide_mode.py b/meanas/fdfd/waveguide_mode.py index 59a3f37..3dbdfd8 100644 --- a/meanas/fdfd/waveguide_mode.py +++ b/meanas/fdfd/waveguide_mode.py @@ -12,7 +12,6 @@ def solve_waveguide_mode_2d(mode_number: int, dxes: dx_lists_t, epsilon: vfield_t, mu: vfield_t = None, - dx_prop: float = 0, ) -> Dict[str, complex or field_t]: """ Given a 2d region, attempts to solve for the eigenmode with the specified mode number. @@ -22,8 +21,6 @@ def solve_waveguide_mode_2d(mode_number: int, :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types :param epsilon: Dielectric constant :param mu: Magnetic permeability (default 1 everywhere) - :param dx_prop: The cell width in the the propagation direction, used to apply a - correction to the wavenumber. Default 0 (i.e. continuous propagation direction) :return: {'E': List[numpy.ndarray], 'H': List[numpy.ndarray], 'wavenumber': complex} """ @@ -49,12 +46,6 @@ def solve_waveguide_mode_2d(mode_number: int, e, h = waveguide.normalized_fields(v, wavenumber, omega, dxes, epsilon, mu) - ''' - Perform correction on wavenumber to account for numerical dispersion. - ''' - if dx_prop != 0: - wavenumber = 2 / dx_prop * numpy.sin(wavenumber * dx_prop / 2) - shape = [d.size for d in dxes[0]] fields = { 'wavenumber': wavenumber, @@ -110,7 +101,6 @@ def solve_waveguide_mode(mode_number: int, 'dxes': [[dx[i][slices[i]] for i in order[:2]] for dx in dxes], 'epsilon': vec([epsilon[i][slices].transpose(order) for i in order]), 'mu': vec([mu[i][slices].transpose(order) for i in order]), - 'dx_prop': dx_prop, } fields_2d = solve_waveguide_mode_2d(mode_number, omega=omega, **args_2d) From 278790864088c3a55d9306ed5688c0f7ab5040ee Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 26 Aug 2019 00:21:39 -0700 Subject: [PATCH 65/77] Add E variants of waveguide equations rename 2d vector from v to e_xy or h_xy --- meanas/fdfd/waveguide.py | 1 + 1 file changed, 1 insertion(+) diff --git a/meanas/fdfd/waveguide.py b/meanas/fdfd/waveguide.py index 91b023c..611cb57 100644 --- a/meanas/fdfd/waveguide.py +++ b/meanas/fdfd/waveguide.py @@ -17,6 +17,7 @@ As the z-dependence is known, all the functions in this file assume a 2D grid (ie. dxes = [[[dx_e_0, dx_e_1, ...], [dy_e_0, ...]], [[dx_h_0, ...], [dy_h_0, ...]]]) with propagation along the z axis. """ +# TODO update module docs from typing import List, Tuple import numpy From 41bec05d4efebb82f2ab268fa27066cbb9eb8833 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 26 Aug 2019 00:24:17 -0700 Subject: [PATCH 66/77] Remove unwanted return --- meanas/fdfd/operators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meanas/fdfd/operators.py b/meanas/fdfd/operators.py index 13b2691..c2df8ab 100644 --- a/meanas/fdfd/operators.py +++ b/meanas/fdfd/operators.py @@ -545,4 +545,4 @@ def e_boundary_source(mask: vfield_t, r3 = sparse.block_diag((r, r, r)) jmask = numpy.logical_or(jmask, numpy.abs(r3 @ mask)) - return sparse.diags(jmask.astype(int)) @ full, jmask + return sparse.diags(jmask.astype(int)) @ full From af8efd00eb410542dbdbd2bf3cc58a7828864cb4 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 26 Aug 2019 00:25:36 -0700 Subject: [PATCH 67/77] Add E-field versions of waveguide mode operators, rename v->e_xy or h_xy, and add ability to specify mode margin in solve_waveguide_mode_2d --- meanas/fdfd/waveguide.py | 205 ++++++++++++++++++++++++++-------- meanas/fdfd/waveguide_mode.py | 16 ++- 2 files changed, 170 insertions(+), 51 deletions(-) diff --git a/meanas/fdfd/waveguide.py b/meanas/fdfd/waveguide.py index 611cb57..63ecb98 100644 --- a/meanas/fdfd/waveguide.py +++ b/meanas/fdfd/waveguide.py @@ -31,11 +31,36 @@ from . import operators __author__ = 'Jan Petykiewicz' -def operator(omega: complex, +def operator_e(omega: complex, dxes: dx_lists_t, epsilon: vfield_t, mu: vfield_t = None, ) -> sparse.spmatrix: + if numpy.any(numpy.equal(mu, None)): + mu = numpy.ones_like(epsilon) + + Dfx, Dfy = operators.deriv_forward(dxes[0]) + Dbx, Dby = operators.deriv_back(dxes[1]) + + eps_parts = numpy.split(epsilon, 3) + eps_xy = sparse.diags(numpy.hstack((eps_parts[0], eps_parts[1]))) + eps_z_inv = sparse.diags(1 / eps_parts[2]) + + mu_parts = numpy.split(mu, 3) + mu_yx = sparse.diags(numpy.hstack((mu_parts[1], mu_parts[0]))) + mu_z_inv = sparse.diags(1 / mu_parts[2]) + + op = omega * omega * mu_yx @ eps_xy + \ + mu_yx @ sparse.vstack((-Dby, Dbx)) @ mu_z_inv @ sparse.hstack((-Dfy, Dfx)) + \ + sparse.vstack((Dfx, Dfy)) @ eps_z_inv @ sparse.hstack((Dbx, Dby)) @ eps_xy + return op + + +def operator_h(omega: complex, + dxes: dx_lists_t, + epsilon: vfield_t, + mu: vfield_t = None, + ) -> sparse.spmatrix: """ Waveguide operator of the form @@ -71,27 +96,27 @@ def operator(omega: complex, mu_xy = sparse.diags(numpy.hstack((mu_parts[0], mu_parts[1]))) mu_z_inv = sparse.diags(1 / mu_parts[2]) - op = omega ** 2 * eps_yx @ mu_xy + \ + op = omega * omega * eps_yx @ mu_xy + \ eps_yx @ sparse.vstack((-Dfy, Dfx)) @ eps_z_inv @ sparse.hstack((-Dby, Dbx)) + \ sparse.vstack((Dbx, Dby)) @ mu_z_inv @ sparse.hstack((Dfx, Dfy)) @ mu_xy return op -def normalized_fields(v: numpy.ndarray, - wavenumber: complex, - omega: complex, - dxes: dx_lists_t, - epsilon: vfield_t, - mu: vfield_t = None, - dx_prop: float = 0, - ) -> Tuple[vfield_t, vfield_t]: +def normalized_fields_e(e_xy: numpy.ndarray, + wavenumber: complex, + omega: complex, + dxes: dx_lists_t, + epsilon: vfield_t, + mu: vfield_t = None, + dx_prop: float = 0, + ) -> Tuple[vfield_t, vfield_t]: """ - Given a vector v containing the vectorized H_x and H_y fields, + Given a vector e_xy containing the vectorized E_x and E_y fields, returns normalized, vectorized E and H fields for the system. - :param v: Vector containing H_x and H_y fields - :param wavenumber: Wavenumber satisfying A @ v == wavenumber**2 * v + :param e_xy: Vector containing E_x and E_y fields + :param wavenumber: Wavenumber satisfying `operator_e(...) @ e_xy == wavenumber**2 * e_xy` :param omega: The angular frequency of the system :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types (2D) :param epsilon: Vectorized dielectric constant grid @@ -99,9 +124,51 @@ def normalized_fields(v: numpy.ndarray, :param dxes_prop: Grid cell width in the propagation direction. Default 0 (continuous). :return: Normalized, vectorized (e, h) containing all vector components. """ - e = v2e(v, wavenumber, omega, dxes, epsilon, mu=mu) - h = v2h(v, wavenumber, dxes, mu=mu) + e = exy2e(wavenumber=wavenumber, dxes=dxes, epsilon=epsilon) @ e_xy + h = exy2h(wavenumber=wavenumber, omega=omega, dxes=dxes, epsilon=epsilon, mu=mu) @ e_xy + e_norm, h_norm = _normalized_fields(e=e, h=h, wavenumber=wavenumber, omega=omega, + dxes=dxes, epsilon=epsilon, mu=mu, dx_prop=dx_prop) + return e_norm, h_norm + +def normalized_fields_h(h_xy: numpy.ndarray, + wavenumber: complex, + omega: complex, + dxes: dx_lists_t, + epsilon: vfield_t, + mu: vfield_t = None, + dx_prop: float = 0, + ) -> Tuple[vfield_t, vfield_t]: + """ + Given a vector e_xy containing the vectorized E_x and E_y fields, + returns normalized, vectorized E and H fields for the system. + + :param e_xy: Vector containing E_x and E_y fields + :param wavenumber: Wavenumber satisfying `operator_e(...) @ e_xy == wavenumber**2 * e_xy` + :param omega: The angular frequency of the system + :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types (2D) + :param epsilon: Vectorized dielectric constant grid + :param mu: Vectorized magnetic permeability grid (default 1 everywhere) + :param dxes_prop: Grid cell width in the propagation direction. Default 0 (continuous). + :return: Normalized, vectorized (e, h) containing all vector components. + """ + e = hxy2e(wavenumber=wavenumber, omega=omega, dxes=dxes, epsilon=epsilon, mu=mu) @ h_xy + h = hxy2h(wavenumber=wavenumber, dxes=dxes, mu=mu) @ h_xy + e_norm, h_norm = _normalized_fields(e=e, h=h, wavenumber=wavenumber, omega=omega, + dxes=dxes, epsilon=epsilon, mu=mu, dx_prop=dx_prop) + return e_norm, h_norm + + +def _normalized_fields(e: numpy.ndarray, + h: numpy.ndarray, + wavenumber: complex, + omega: complex, + dxes: dx_lists_t, + epsilon: vfield_t, + mu: vfield_t = None, + dx_prop: float = 0, + ) -> Tuple[vfield_t, vfield_t]: + # TODO documentation shape = [s.size for s in dxes[0]] dxes_real = [[numpy.real(d) for d in numpy.meshgrid(*dxes[v], indexing='ij')] for v in (0, 1)] @@ -131,56 +198,104 @@ def normalized_fields(v: numpy.ndarray, return e, h -def v2h(v: numpy.ndarray, - wavenumber: complex, - dxes: dx_lists_t, - mu: vfield_t = None - ) -> vfield_t: +def exy2h(wavenumber: complex, + omega: complex, + dxes: dx_lists_t, + epsilon: vfield_t, + mu: vfield_t = None + ) -> sparse.spmatrix: """ - Given a vector v containing the vectorized H_x and H_y fields, - returns a vectorized H including all three H components. + Operator which transforms the vector e_xy containing the vectorized E_x and E_y fields, + into a vectorized H containing all three H components - :param v: Vector containing H_x and H_y fields - :param wavenumber: Wavenumber satisfying A @ v == wavenumber**2 * v + :param wavenumber: Wavenumber satisfying `operator_e(...) @ e_xy == wavenumber**2 * e_xy` + :param omega: The angular frequency of the system + :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types (2D) + :param epsilon: Vectorized dielectric constant grid + :param mu: Vectorized magnetic permeability grid (default 1 everywhere) + :return: Sparse matrix representing the operator + """ + e2hop = e2h(wavenumber=wavenumber, omega=omega, dxes=dxes, mu=mu) + return e2hop @ exy2e(wavenumber=wavenumber, dxes=dxes, epsilon=epsilon) + + +def hxy2e(wavenumber: complex, + omega: complex, + dxes: dx_lists_t, + epsilon: vfield_t, + mu: vfield_t = None + ) -> sparse.spmatrix: + """ + Operator which transforms the vector h_xy containing the vectorized H_x and H_y fields, + into a vectorized E containing all three E components + + :param wavenumber: Wavenumber satisfying `operator_h(...) @ h_xy == wavenumber**2 * h_xy` + :param omega: The angular frequency of the system + :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types (2D) + :param epsilon: Vectorized dielectric constant grid + :param mu: Vectorized magnetic permeability grid (default 1 everywhere) + :return: Sparse matrix representing the operator + """ + h2eop = h2e(wavenumber=wavenumber, omega=omega, dxes=dxes, epsilon=epsilon) + return h2eop @ hxy2h(wavenumber=wavenumber, dxes=dxes, mu=mu) + + +def hxy2h(wavenumber: complex, + dxes: dx_lists_t, + mu: vfield_t = None + ) -> sparse.spmatrix: + """ + Operator which transforms the vector h_xy containing the vectorized H_x and H_y fields, + into a vectorized H containing all three H components + + :param wavenumber: Wavenumber satisfying `operator_h(...) @ h_xy == wavenumber**2 * h_xy` :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types (2D) :param mu: Vectorized magnetic permeability grid (default 1 everywhere) - :return: Vectorized H field with all vector components + :return: Sparse matrix representing the operator """ Dfx, Dfy = operators.deriv_forward(dxes[0]) - op = sparse.hstack((Dfx, Dfy)) + hxy2hz = sparse.hstack((Dfx, Dfy)) / (1j * wavenumber) if not numpy.any(numpy.equal(mu, None)): mu_parts = numpy.split(mu, 3) mu_xy = sparse.diags(numpy.hstack((mu_parts[0], mu_parts[1]))) mu_z_inv = sparse.diags(1 / mu_parts[2]) - op = mu_z_inv @ op @ mu_xy + hxy2hz = mu_z_inv @ hxy2hz @ mu_xy - w = op @ v / (1j * wavenumber) - return numpy.hstack((v, w)).flatten() + n_pts = dxes[1][0].size * dxes[1][1].size + op = sparse.vstack((sparse.eye(2 * n_pts), + hxy2hz)) + return op -def v2e(v: numpy.ndarray, - wavenumber: complex, - omega: complex, - dxes: dx_lists_t, - epsilon: vfield_t, - mu: vfield_t = None - ) -> vfield_t: +def exy2e(wavenumber: complex, + dxes: dx_lists_t, + epsilon: vfield_t, + ) -> sparse.spmatrix: """ - Given a vector v containing the vectorized H_x and H_y fields, - returns a vectorized E containing all three E components + Operator which transforms the vector e_xy containing the vectorized E_x and E_y fields, + into a vectorized E containing all three E components - :param v: Vector containing H_x and H_y fields - :param wavenumber: Wavenumber satisfying A @ v == wavenumber**2 * v - :param omega: The angular frequency of the system + :param wavenumber: Wavenumber satisfying `operator_e(...) @ e_xy == wavenumber**2 * e_xy` :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types (2D) :param epsilon: Vectorized dielectric constant grid - :param mu: Vectorized magnetic permeability grid (default 1 everywhere) - :return: Vectorized E field with all vector components. + :return: Sparse matrix representing the operator """ - h2eop = h2e(wavenumber, omega, dxes, epsilon) - return h2eop @ v2h(v, wavenumber, dxes, mu) + Dbx, Dby = operators.deriv_back(dxes[1]) + exy2ez = sparse.hstack((Dbx, Dby)) / (1j * wavenumber) + + if not numpy.any(numpy.equal(epsilon, None)): + epsilon_parts = numpy.split(epsilon, 3) + epsilon_xy = sparse.diags(numpy.hstack((epsilon_parts[0], epsilon_parts[1]))) + epsilon_z_inv = sparse.diags(1 / epsilon_parts[2]) + + exy2ez = epsilon_z_inv @ exy2ez @ epsilon_xy + + n_pts = dxes[0][0].size * dxes[0][1].size + op = sparse.vstack((sparse.eye(2 * n_pts), + exy2ez)) + return op def e2h(wavenumber: complex, diff --git a/meanas/fdfd/waveguide_mode.py b/meanas/fdfd/waveguide_mode.py index 3dbdfd8..300d3e6 100644 --- a/meanas/fdfd/waveguide_mode.py +++ b/meanas/fdfd/waveguide_mode.py @@ -12,6 +12,7 @@ def solve_waveguide_mode_2d(mode_number: int, dxes: dx_lists_t, epsilon: vfield_t, mu: vfield_t = None, + mode_margin: int = 2, ) -> Dict[str, complex or field_t]: """ Given a 2d region, attempts to solve for the eigenmode with the specified mode number. @@ -21,6 +22,9 @@ def solve_waveguide_mode_2d(mode_number: int, :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types :param epsilon: Dielectric constant :param mu: Magnetic permeability (default 1 everywhere) + :param mode_margin: The eigensolver will actually solve for (mode_number + mode_margin) + modes, but only return the target mode. Increasing this value can improve the solver's + ability to find the correct mode. Default 2. :return: {'E': List[numpy.ndarray], 'H': List[numpy.ndarray], 'wavenumber': complex} """ @@ -28,23 +32,23 @@ def solve_waveguide_mode_2d(mode_number: int, Solve for the largest-magnitude eigenvalue of the real operator ''' dxes_real = [[numpy.real(dx) for dx in dxi] for dxi in dxes] - A_r = waveguide.operator(numpy.real(omega), dxes_real, numpy.real(epsilon), numpy.real(mu)) + A_r = waveguide.operator_e(numpy.real(omega), dxes_real, numpy.real(epsilon), numpy.real(mu)) - eigvals, eigvecs = signed_eigensolve(A_r, mode_number+3) - v = eigvecs[:, -(mode_number + 1)] + eigvals, eigvecs = signed_eigensolve(A_r, mode_number + mode_margin) + exy = eigvecs[:, -(mode_number + 1)] ''' Now solve for the eigenvector of the full operator, using the real operator's eigenvector as an initial guess for Rayleigh quotient iteration. ''' - A = waveguide.operator(omega, dxes, epsilon, mu) - eigval, v = rayleigh_quotient_iteration(A, v) + A = waveguide.operator_e(omega, dxes, epsilon, mu) + eigval, exy = rayleigh_quotient_iteration(A, exy) # Calculate the wave-vector (force the real part to be positive) wavenumber = numpy.sqrt(eigval) wavenumber *= numpy.sign(numpy.real(wavenumber)) - e, h = waveguide.normalized_fields(v, wavenumber, omega, dxes, epsilon, mu) + e, h = waveguide.normalized_fields_e(exy, wavenumber, omega, dxes, epsilon, mu) shape = [d.size for d in dxes[0]] fields = { From c306bb1f46c40e7f4aad32d4cd6591cd8345b0d5 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 26 Aug 2019 00:26:54 -0700 Subject: [PATCH 68/77] Correct for numerical dispersion at 3d solve_waveguide_mode level --- meanas/fdfd/waveguide_mode.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/meanas/fdfd/waveguide_mode.py b/meanas/fdfd/waveguide_mode.py index 300d3e6..2f32c89 100644 --- a/meanas/fdfd/waveguide_mode.py +++ b/meanas/fdfd/waveguide_mode.py @@ -111,6 +111,9 @@ def solve_waveguide_mode(mode_number: int, ''' Apply corrections and expand to 3D ''' + # Correct wavenumber to account for numerical dispersion. + fields_2d['wavenumber'] = 2/dx_prop * numpy.arcsin(fields_2d['wavenumber'] * dx_prop/2) + # Adjust for propagation direction fields_2d['E'][2] *= polarity fields_2d['H'][2] *= polarity From 7006b5e6e4c73c81284bf73009968ac53cc1df0b Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 26 Aug 2019 00:27:05 -0700 Subject: [PATCH 69/77] Flip propagation direction by flipping H --- meanas/fdfd/waveguide_mode.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/meanas/fdfd/waveguide_mode.py b/meanas/fdfd/waveguide_mode.py index 2f32c89..7776ce2 100644 --- a/meanas/fdfd/waveguide_mode.py +++ b/meanas/fdfd/waveguide_mode.py @@ -115,8 +115,7 @@ def solve_waveguide_mode(mode_number: int, fields_2d['wavenumber'] = 2/dx_prop * numpy.arcsin(fields_2d['wavenumber'] * dx_prop/2) # Adjust for propagation direction - fields_2d['E'][2] *= polarity - fields_2d['H'][2] *= polarity + fields_2d['H'] *= polarity # Apply phase shift to H-field fields_2d['H'] *= numpy.exp(-polarity * 1j * 0.5 * fields_2d['wavenumber'] * dx_prop) From 1860d754cda26da183bebb814c5fe23748787111 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 26 Aug 2019 00:27:32 -0700 Subject: [PATCH 70/77] Fix shifts applied to E and H fields Only some components need shifting --- meanas/fdfd/waveguide_mode.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/meanas/fdfd/waveguide_mode.py b/meanas/fdfd/waveguide_mode.py index 7776ce2..b9524e5 100644 --- a/meanas/fdfd/waveguide_mode.py +++ b/meanas/fdfd/waveguide_mode.py @@ -118,7 +118,8 @@ def solve_waveguide_mode(mode_number: int, fields_2d['H'] *= polarity # Apply phase shift to H-field - fields_2d['H'] *= numpy.exp(-polarity * 1j * 0.5 * fields_2d['wavenumber'] * dx_prop) + fields_2d['H'][:2] *= numpy.exp(-1j * polarity * 0.5 * fields_2d['wavenumber'] * dx_prop) + fields_2d['E'][2] *= numpy.exp(-1j * polarity * 0.5 * fields_2d['wavenumber'] * dx_prop) # Expand E, H to full epsilon space we were given E = numpy.zeros_like(epsilon, dtype=complex) From d6a34b280eb22e5ff06ff1785fa356ab24ef93ff Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 26 Aug 2019 00:28:06 -0700 Subject: [PATCH 71/77] Simplify compute_source and fix propagation direction --- meanas/fdfd/waveguide_mode.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/meanas/fdfd/waveguide_mode.py b/meanas/fdfd/waveguide_mode.py index b9524e5..cd59b5f 100644 --- a/meanas/fdfd/waveguide_mode.py +++ b/meanas/fdfd/waveguide_mode.py @@ -178,10 +178,10 @@ def compute_source(E: field_t, s2[axis] = slice(slices[axis].start, slices[axis].stop) s2 = (src_order, *s2) - rollby = 1 if polarity > 0 else 0 - exp_iphi = numpy.exp(-1j * polarity * wavenumber * dxes[1][axis][slices[axis]]) - J[s2] = numpy.roll(functional.curl_h(dxes=dxes)(H.conj()), -rollby, axis=axis+1)[s2] * polarity * exp_iphi - M[s2] = -numpy.roll(functional.curl_e(dxes=dxes)(E.conj()), rollby, axis=axis+1)[s2] + rollby = 1 if polarity < 0 else 0 + exp_iphi = numpy.exp(-1j * -rollby * wavenumber * dxes[1][axis][slices[axis]]) + J[s2] = numpy.roll(functional.curl_h(dxes=dxes)(H), -rollby, axis=axis+1)[s2] * exp_iphi * -polarity + M[s2] = numpy.roll(functional.curl_e(dxes=dxes)(E), rollby, axis=axis+1)[s2] m2j = functional.m2j(omega, dxes, mu) Jm = m2j(M) From 3887a8804f7767214fe2244f3ade32f60c9b9e75 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 26 Aug 2019 00:28:19 -0700 Subject: [PATCH 72/77] fix phase in expand_wgmode --- meanas/fdfd/waveguide_mode.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/meanas/fdfd/waveguide_mode.py b/meanas/fdfd/waveguide_mode.py index cd59b5f..bda7fcf 100644 --- a/meanas/fdfd/waveguide_mode.py +++ b/meanas/fdfd/waveguide_mode.py @@ -458,7 +458,7 @@ def expand_wgmode_e(E: field_t, a_shape = numpy.roll([1, -1, 1, 1], axis) a_E = numpy.real(dxes[0][axis]).cumsum() r_E = a_E - a_E[slices[axis]] - iphi = polarity * 1j * wavenumber + iphi = polarity * -1j * wavenumber phase_E = numpy.exp(iphi * r_E).reshape(a_shape) # Expand our slice to the entire grid using the phase factors From 7b56aa363f506759a5133e87b4cf8c60372c25d5 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 26 Aug 2019 01:02:54 -0700 Subject: [PATCH 73/77] Use non-vectorized fields for waveguide_mode functions --- meanas/fdfd/waveguide_mode.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/meanas/fdfd/waveguide_mode.py b/meanas/fdfd/waveguide_mode.py index bda7fcf..fc25c70 100644 --- a/meanas/fdfd/waveguide_mode.py +++ b/meanas/fdfd/waveguide_mode.py @@ -10,8 +10,8 @@ from ..eigensolvers import signed_eigensolve, rayleigh_quotient_iteration def solve_waveguide_mode_2d(mode_number: int, omega: complex, dxes: dx_lists_t, - epsilon: vfield_t, - mu: vfield_t = None, + epsilon: field_t, + mu: field_t = None, mode_margin: int = 2, ) -> Dict[str, complex or field_t]: """ @@ -25,14 +25,14 @@ def solve_waveguide_mode_2d(mode_number: int, :param mode_margin: The eigensolver will actually solve for (mode_number + mode_margin) modes, but only return the target mode. Increasing this value can improve the solver's ability to find the correct mode. Default 2. - :return: {'E': List[numpy.ndarray], 'H': List[numpy.ndarray], 'wavenumber': complex} + :return: {'E': numpy.ndarray, 'H': numpy.ndarray, 'wavenumber': complex} """ ''' Solve for the largest-magnitude eigenvalue of the real operator ''' dxes_real = [[numpy.real(dx) for dx in dxi] for dxi in dxes] - A_r = waveguide.operator_e(numpy.real(omega), dxes_real, numpy.real(epsilon), numpy.real(mu)) + A_r = waveguide.operator_e(numpy.real(omega), dxes_real, vec(numpy.real(epsilon)), vec(numpy.real(mu))) eigvals, eigvecs = signed_eigensolve(A_r, mode_number + mode_margin) exy = eigvecs[:, -(mode_number + 1)] @@ -41,14 +41,14 @@ def solve_waveguide_mode_2d(mode_number: int, Now solve for the eigenvector of the full operator, using the real operator's eigenvector as an initial guess for Rayleigh quotient iteration. ''' - A = waveguide.operator_e(omega, dxes, epsilon, mu) + A = waveguide.operator_e(omega, dxes, vec(epsilon), vec(mu)) eigval, exy = rayleigh_quotient_iteration(A, exy) # Calculate the wave-vector (force the real part to be positive) wavenumber = numpy.sqrt(eigval) wavenumber *= numpy.sign(numpy.real(wavenumber)) - e, h = waveguide.normalized_fields_e(exy, wavenumber, omega, dxes, epsilon, mu) + e, h = waveguide.normalized_fields_e(exy, wavenumber, omega, dxes, vec(epsilon), vec(mu)) shape = [d.size for d in dxes[0]] fields = { @@ -103,8 +103,8 @@ def solve_waveguide_mode(mode_number: int, # Reduce to 2D and solve the 2D problem args_2d = { 'dxes': [[dx[i][slices[i]] for i in order[:2]] for dx in dxes], - 'epsilon': vec([epsilon[i][slices].transpose(order) for i in order]), - 'mu': vec([mu[i][slices].transpose(order) for i in order]), + 'epsilon': [epsilon[i][slices].transpose(order) for i in order], + 'mu': [mu[i][slices].transpose(order) for i in order], } fields_2d = solve_waveguide_mode_2d(mode_number, omega=omega, **args_2d) From 5f96474497d22c5ef8263f634cbfd43e300a84d7 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 26 Aug 2019 01:03:13 -0700 Subject: [PATCH 74/77] Use e_boundary_source for compute_source --- meanas/fdfd/waveguide_mode.py | 38 ++++++++++++----------------------- 1 file changed, 13 insertions(+), 25 deletions(-) diff --git a/meanas/fdfd/waveguide_mode.py b/meanas/fdfd/waveguide_mode.py index fc25c70..410ee02 100644 --- a/meanas/fdfd/waveguide_mode.py +++ b/meanas/fdfd/waveguide_mode.py @@ -137,13 +137,13 @@ def solve_waveguide_mode(mode_number: int, def compute_source(E: field_t, - H: field_t, wavenumber: complex, omega: complex, dxes: dx_lists_t, axis: int, polarity: int, slices: List[slice], + epsilon: field_t, mu: field_t = None, ) -> field_t: """ @@ -151,7 +151,6 @@ def compute_source(E: field_t, necessary to position a unidirectional source at the slice location. :param E: E-field of the mode - :param H: H-field of the mode (advanced by half of a Yee cell from E) :param wavenumber: Wavenumber of the mode :param omega: Angular frequency of the simulation :param dxes: Grid parameters [dx_e, dx_h] as described in meanas.types @@ -162,32 +161,21 @@ def compute_source(E: field_t, :param mu: Magnetic permeability (default 1 everywhere) :return: J distribution for the unidirectional source """ - J = numpy.zeros_like(E, dtype=complex) - M = numpy.zeros_like(E, dtype=complex) + E_expanded = expand_wgmode_e(E=E, dxes=dxes, wavenumber=wavenumber, axis=axis, + polarity=polarity, slices=slices) - src_order = numpy.roll(range(3), -axis) + smask = [slice(None)] * 4 + if polarity > 0: + smask[axis + 1] = slice(slices[axis].start, None) + else: + smask[axis + 1] = slice(None, slices[axis].stop) -# exp_iphi = numpy.exp(1j * polarity * wavenumber * dxes[1][axis][slices[axis]]) -# rollby = -1 if polarity > 0 else 0 -# J[src_order[1]] = +exp_iphi * H[src_order[2]] * polarity / dxes[1][axis][slices[axis]] -# J[src_order[2]] = -exp_iphi * H[src_order[1]] * polarity / dxes[1][axis][slices[axis]] -# M[src_order[1]] = +numpy.roll(E[src_order[2]], rollby, axis=axis) / dxes[0][axis][slices[axis]] -# M[src_order[2]] = -numpy.roll(E[src_order[1]], rollby, axis=axis) / dxes[0][axis][slices[axis]] + mask = numpy.zeros_like(E_expanded, dtype=int) + mask[tuple(smask)] = 1 - s2 = [slice(None), slice(None), slice(None)] - s2[axis] = slice(slices[axis].start, slices[axis].stop) - s2 = (src_order, *s2) - - rollby = 1 if polarity < 0 else 0 - exp_iphi = numpy.exp(-1j * -rollby * wavenumber * dxes[1][axis][slices[axis]]) - J[s2] = numpy.roll(functional.curl_h(dxes=dxes)(H), -rollby, axis=axis+1)[s2] * exp_iphi * -polarity - M[s2] = numpy.roll(functional.curl_e(dxes=dxes)(E), rollby, axis=axis+1)[s2] - - m2j = functional.m2j(omega, dxes, mu) - Jm = m2j(M) - - Jtot = J + Jm - return Jtot + masked_e2j = operators.e_boundary_source(mask=vec(mask), omega=omega, dxes=dxes, epsilon=vec(epsilon), mu=vec(mu)) + J = unvec(masked_e2j @ vec(E_expanded), E.shape[1:]) + return J def compute_overlap_e(E: field_t, From 337cee801803d3975b08437a3543402208491bfb Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 26 Aug 2019 01:10:54 -0700 Subject: [PATCH 75/77] Add epsilon arg to compute_overlap_e currently unused but useful for reusing solve_wgmode arguments --- meanas/fdfd/waveguide_mode.py | 1 + 1 file changed, 1 insertion(+) diff --git a/meanas/fdfd/waveguide_mode.py b/meanas/fdfd/waveguide_mode.py index 410ee02..35439a0 100644 --- a/meanas/fdfd/waveguide_mode.py +++ b/meanas/fdfd/waveguide_mode.py @@ -186,6 +186,7 @@ def compute_overlap_e(E: field_t, axis: int, polarity: int, slices: List[slice], + epsilon: field_t, # TODO unused?? mu: field_t = None, ) -> field_t: """ From d2d4220313c2413b83692fbc9b64b1c8ed2556a8 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 26 Aug 2019 01:12:36 -0700 Subject: [PATCH 76/77] update example code --- examples/fdfd.py | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/examples/fdfd.py b/examples/fdfd.py index c20fc3e..f5130be 100644 --- a/examples/fdfd.py +++ b/examples/fdfd.py @@ -3,7 +3,7 @@ import numpy from numpy.linalg import norm import meanas -from meanas import vec, unvec +from meanas import vec, unvec, fdtd from meanas.fdfd import waveguide_mode, functional, scpml, operators from meanas.fdfd.solvers import generic as generic_solver @@ -125,16 +125,18 @@ def test1(solver=generic_solver): dims[1][0] = dims[0][0] ind_dims = (grid.pos2ind(dims[0], which_shifts=None).astype(int), grid.pos2ind(dims[1], which_shifts=None).astype(int)) + src_axis = 0 wg_args = { 'omega': omega, 'slices': [slice(i, f+1) for i, f in zip(*ind_dims)], 'dxes': dxes, - 'axis': 0, + 'axis': src_axis, 'polarity': +1, + 'epsilon': grid.grids, } - wg_results = waveguide_mode.solve_waveguide_mode(mode_number=0, **wg_args, epsilon=grid.grids) - J = waveguide_mode.compute_source(**wg_args, **wg_results) + wg_results = waveguide_mode.solve_waveguide_mode(mode_number=0, **wg_args) + J = waveguide_mode.compute_source(**wg_args, E=wg_results['E'], wavenumber=wg_results['wavenumber']) H_overlap = waveguide_mode.compute_overlap_e(**wg_args, **wg_results) pecg = gridlock.Grid(edge_coords, initial=0.0, num_grids=3) @@ -145,6 +147,12 @@ def test1(solver=generic_solver): # pmcg.draw_cuboid(center=[700, 0, 0], dimensions=[80, 1e8, 1e8], eps=1) # pmcg.visualize_isosurface() + def pcolor(v): + vmax = numpy.max(numpy.abs(v)) + pyplot.pcolor(v, cmap='seismic', vmin=-vmax, vmax=vmax) + pyplot.axis('equal') + pyplot.colorbar() + ''' Solve! ''' @@ -167,20 +175,14 @@ def test1(solver=generic_solver): ''' Plot results ''' - def pcolor(v): - vmax = numpy.max(numpy.abs(v)) - pyplot.pcolor(v, cmap='seismic', vmin=-vmax, vmax=vmax) - pyplot.axis('equal') - pyplot.colorbar() - center = grid.pos2ind([0, 0, 0], None).astype(int) pyplot.figure() pyplot.subplot(2, 2, 1) - pcolor(numpy.real(E[1][center[0], :, :])) + pcolor(numpy.real(E[1][center[0], :, :]).T) pyplot.subplot(2, 2, 2) pyplot.plot(numpy.log10(numpy.abs(E[1][:, center[1], center[2]]) + 1e-10)) pyplot.subplot(2, 2, 3) - pcolor(numpy.real(E[1][:, :, center[2]])) + pcolor(numpy.real(E[1][:, :, center[2]]).T) pyplot.subplot(2, 2, 4) def poyntings(E): @@ -213,7 +215,7 @@ def module_available(name): if __name__ == '__main__': - test0() + #test0() if module_available('opencl_fdfd'): from opencl_fdfd import cg_solver as opencl_solver From f4bac9598d78043a73b6a60dbc70441ccb2ad531 Mon Sep 17 00:00:00 2001 From: Jan Petykiewicz Date: Mon, 26 Aug 2019 01:18:44 -0700 Subject: [PATCH 77/77] Remove unused waveguide_mode functions --- meanas/fdfd/waveguide_mode.py | 101 ---------------------------------- 1 file changed, 101 deletions(-) diff --git a/meanas/fdfd/waveguide_mode.py b/meanas/fdfd/waveguide_mode.py index 35439a0..8f90d3e 100644 --- a/meanas/fdfd/waveguide_mode.py +++ b/meanas/fdfd/waveguide_mode.py @@ -307,107 +307,6 @@ def solve_waveguide_mode_cylindrical(mode_number: int, return fields -def compute_source_q(E: field_t, - H: field_t, - wavenumber: complex, - omega: complex, - dxes: dx_lists_t, - axis: int, - polarity: int, - slices: List[slice], - mu: field_t = None, - ) -> field_t: - A1f = functional.curl_h(dxes) - A2f = functional.curl_e(dxes) - - J = A1f(H) - M = A2f(-E) - - m2j = functional.m2j(omega, dxes, mu) - Jm = m2j(M) - - Jtot = J + Jm - return Jtot, J, M - - -def compute_source_e(QE: field_t, - omega: complex, - dxes: dx_lists_t, - axis: int, - polarity: int, - slices: List[slice], - epsilon: field_t, - mu: field_t = None, - ) -> field_t: - """ - Want AQE = -iwJ, where Q is mask and normally AE = -iwJ - ## Want (AQ-QA) E = -iwJ, where Q is a mask - ## If E is an eigenmode, AE = 0 so just AQE = -iwJ - Really only need E in 4 cells along axis (0, 0, Emode1, Emode2), find AE (1 iteration), then use center 2 cells as src - Maybe better to use (0, Emode1, Emode2, Emode3), find AE (1 iteration), then use left 2 cells as src? - """ - slices = tuple(slices) - - # Trim a cell from each end of the propagation axis - slices_reduced = list(slices) - for aa in range(3): - if aa == axis: - if polarity > 0: - slices_reduced[axis] = slice(slices[axis].start, slices[axis].start+2) - else: - slices_reduced[axis] = slice(slices[axis].stop-2, slices[axis].stop) - else: - start = slices[aa].start - stop = slices[aa].stop -# if start is not None or stop is not None: -# if start is None: -# start = 1 -# stop -= 1 -# elif stop is None: -# stop = E.shape[aa + 1] - 1 -# start += 1 -# else: -# start += 1 -# stop -= 1 -# slices_reduced[aa] = slice(start, stop) - slices_reduced = (slice(None), *slices_reduced) - - # Don't actually need to mask out E here since it needs to be pre-masked (QE) - - A = functional.e_full(omega, dxes, epsilon, mu) - J4 = A(QE) / (-1j * omega) #J4 is 4-cell result of -iwJ = A QE - - J = numpy.zeros_like(J4) - J[slices_reduced] = J4[slices_reduced] - return J - - -def compute_source_wg(E: field_t, - wavenumber: complex, - omega: complex, - dxes: dx_lists_t, - axis: int, - polarity: int, - slices: List[slice], - epsilon: field_t, - mu: field_t = None, - ) -> field_t: - slices = tuple(slices) - Etgt, _slices2 = compute_overlap_ce(E=E, wavenumber=wavenumber, - dxes=dxes, axis=axis, polarity=polarity, - slices=slices) - - slices4 = list(slices) - slices4[axis] = slice(slices[axis].start - 4 * polarity, slices[axis].start) - slices4 = tuple(slices4) - - J = compute_source_e(QE=Etgt, - omega=omega, dxes=dxes, axis=axis, - polarity=polarity, slices=slices4, - epsilon=epsilon, mu=mu) - return J - - def compute_overlap_ce(E: field_t, wavenumber: complex, dxes: dx_lists_t,